From 3566386a89922e1e7535038a46dcd44819d82dc1 Mon Sep 17 00:00:00 2001 From: chenzhihui Date: Wed, 17 Feb 2021 17:14:47 +0800 Subject: [PATCH] init v2 --- .github/FUNDING.yml | 3 + .github/workflows/go.yml | 131 - .gitignore | 52 +- .golangci.yml | 138 - .travis.yml | 59 - LICENSE | 2 +- README.md | 115 +- api/README.md | 1 + api/kratos/api/annotations.pb.go | 100 + api/kratos/api/annotations.proto | 13 + app.go | 134 + app_test.go | 25 + cmd/kratos/go.mod | 10 + cmd/kratos/go.sum | 354 + cmd/kratos/internal/base/mod.go | 16 + cmd/kratos/internal/base/path.go | 100 + cmd/kratos/internal/base/repo.go | 71 + cmd/kratos/internal/base/repo_test.go | 16 + cmd/kratos/internal/new/new.go | 36 + cmd/kratos/internal/new/project.go | 38 + cmd/kratos/internal/proto/add/add.go | 65 + cmd/kratos/internal/proto/add/proto.go | 41 + cmd/kratos/internal/proto/add/template.go | 52 + cmd/kratos/internal/proto/proto.go | 27 + cmd/kratos/internal/proto/service/service.go | 86 + cmd/kratos/internal/proto/service/template.go | 56 + cmd/kratos/internal/proto/source/source.go | 28 + cmd/kratos/main.go | 27 + cmd/kratos/version.go | 13 + cmd/protoc-gen-go-errors/errors.go | 58 + cmd/protoc-gen-go-errors/go.mod | 9 + cmd/protoc-gen-go-errors/go.sum | 101 + cmd/protoc-gen-go-errors/main.go | 35 + cmd/protoc-gen-go-errors/template.go | 40 + cmd/protoc-gen-go-http/go.mod | 11 + cmd/protoc-gen-go-http/go.sum | 99 + cmd/protoc-gen-go-http/http.go | 210 + .../internal/testproto/echo_service.pb.go | 669 ++ .../internal/testproto/echo_service.proto | 101 + .../testproto/echo_service_grpc.pb.go | 219 + .../testproto/echo_service_http.pb.go | 240 + cmd/protoc-gen-go-http/main.go | 35 + cmd/protoc-gen-go-http/template.go | 101 + config/README.md | 37 + config/config.go | 141 + config/file/file.go | 79 + config/file/file_test.go | 142 + config/file/watcher.go | 53 + config/options.go | 38 + config/reader.go | 107 + config/source.go | 20 + config/value.go | 105 + docs/.nojekyll | 0 docs/CNAME | 1 - docs/FAQ.md | 21 - docs/README.md | 36 - docs/_sidebar.md | 39 - docs/blademaster-mid.md | 177 - docs/blademaster-mod.md | 88 - docs/blademaster-pb.md | 83 - docs/blademaster-quickstart.md | 142 - docs/blademaster.md | 43 - docs/breaker.md | 49 - docs/cache-mc.md | 195 - docs/cache-redis.md | 181 - docs/cache.md | 20 - docs/config-paladin.md | 118 - docs/config.md | 48 - docs/database-hbase.md | 51 - docs/database-mysql-orm.md | 42 - docs/database-mysql.md | 195 - docs/database-tidb.md | 0 docs/database.md | 19 - docs/design/kratos-v2.md | 14 + docs/ecode.md | 102 - docs/images/alipay.png | Bin 0 -> 42462 bytes docs/{img/kratos3.png => images/kratos.png} | Bin docs/img/bm-arch-2-2.png | Bin 9888 -> 0 bytes docs/img/bm-arch-2-3.png | Bin 31327 -> 0 bytes docs/img/bm-handlers.png | Bin 25036 -> 0 bytes docs/img/kratos-log.jpg | Bin 35334 -> 0 bytes docs/img/kratos.png | Bin 7429 -> 0 bytes docs/img/kratos2.png | Bin 7963 -> 0 bytes docs/img/ratelimit-benchmark-up-1.png | Bin 676504 -> 0 bytes docs/img/ratelimit-rolling-window.png | Bin 19131 -> 0 bytes docs/img/zipkin.jpg | Bin 65836 -> 0 bytes docs/index.html | 30 - docs/install.md | 66 - docs/kratos-genbts.md | 27 - docs/kratos-genmc.md | 68 - docs/kratos-protoc.md | 28 - docs/kratos-swagger.md | 8 - docs/kratos-tool.md | 106 - docs/log-agent.md | 0 docs/logger.md | 34 - docs/protoc.md | 26 - docs/quickstart.md | 71 - docs/ratelimit.md | 57 - docs/trace.md | 42 - docs/ut-support.md | 500 -- docs/ut-testcli.md | 152 - docs/ut-testgen.md | 45 - docs/ut.md | 32 - docs/warden-balancer.md | 39 - docs/warden-mid.md | 374 -- docs/warden-pb.md | 48 - docs/warden-quickstart.md | 171 - docs/warden-resolver.md | 254 - docs/warden.md | 41 - encoding/encoding.go | 40 + encoding/json/json.go | 49 + encoding/proto/proto.go | 30 + errors/codes.go | 285 + errors/errors.go | 73 + errors/errors.pb.go | 187 + errors/errors.proto | 19 + errors/errors_test.go | 54 + example/blademaster/middleware/auth/README.md | 7 - example/blademaster/middleware/auth/auth.go | 153 - .../middleware/auth/example_test.go | 40 - example/protobuf/api.bm.go | 54 - example/protobuf/api.ecode.go | 17 - example/protobuf/api.pb.go | 1000 --- example/protobuf/api.proto | 35 - example/protobuf/api.swagger.json | 96 - example/protobuf/gen.sh | 3 - go.mod | 78 +- go.sum | 529 +- internal/README.md | 1 + internal/host/host.go | 72 + internal/host/host_test.go | 63 + log/README.md | 15 + log/helper.go | 86 + log/helper_test.go | 20 + log/level.go | 40 + log/log.go | 48 + log/log_test.go | 13 + log/nop.go | 5 + log/std.go | 45 + log/std_test.go | 12 + metrics/README.md | 2 + metrics/metrics.go | 22 + middleware/logging/logging.go | 118 + middleware/middleware.go | 21 + middleware/recovery/recovery.go | 64 + middleware/status/status.go | 113 + middleware/status/status_test.go | 21 + middleware/tracing/tracing.go | 128 + misc/stat/dashboard/README.md | 15 - misc/stat/dashboard/prometheus.json | 5917 ----------------- options.go | 79 + pkg/cache/memcache/ascii_conn.go | 262 - pkg/cache/memcache/ascii_conn_test.go | 567 -- pkg/cache/memcache/conn.go | 287 - pkg/cache/memcache/conn_test.go | 185 - pkg/cache/memcache/encoding.go | 162 - pkg/cache/memcache/encoding_test.go | 220 - pkg/cache/memcache/errors.go | 79 - pkg/cache/memcache/example_test.go | 177 - pkg/cache/memcache/main_test.go | 85 - pkg/cache/memcache/memcache.go | 377 -- pkg/cache/memcache/memcache_test.go | 300 - pkg/cache/memcache/metrics.go | 51 - pkg/cache/memcache/pool_conn.go | 203 - pkg/cache/memcache/pool_conn_test.go | 543 -- pkg/cache/memcache/test/docker-compose.yaml | 9 - pkg/cache/memcache/test/test.pb.go | 375 -- pkg/cache/memcache/test/test.proto | 12 - pkg/cache/memcache/trace_conn.go | 103 - pkg/cache/memcache/util.go | 88 - pkg/cache/memcache/util_test.go | 105 - pkg/cache/metrics.go | 23 - pkg/cache/redis/README.md | 7 - pkg/cache/redis/commandinfo.go | 57 - pkg/cache/redis/commandinfo_test.go | 27 - pkg/cache/redis/conn.go | 609 -- pkg/cache/redis/conn_test.go | 670 -- pkg/cache/redis/doc.go | 169 - pkg/cache/redis/errors.go | 43 - pkg/cache/redis/log.go | 123 - pkg/cache/redis/main_test.go | 67 - pkg/cache/redis/metrics.go | 53 - pkg/cache/redis/mock.go | 36 - pkg/cache/redis/pipeline.go | 84 - pkg/cache/redis/pipeline_test.go | 96 - pkg/cache/redis/pool.go | 240 - pkg/cache/redis/pool_test.go | 539 -- pkg/cache/redis/pubsub.go | 152 - pkg/cache/redis/pubsub_test.go | 146 - pkg/cache/redis/redis.go | 79 - pkg/cache/redis/redis_test.go | 323 - pkg/cache/redis/reply.go | 409 -- pkg/cache/redis/reply_test.go | 179 - pkg/cache/redis/scan.go | 558 -- pkg/cache/redis/scan_test.go | 435 -- pkg/cache/redis/script.go | 86 - pkg/cache/redis/script_test.go | 102 - pkg/cache/redis/test/docker-compose.yaml | 12 - pkg/cache/redis/trace.go | 152 - pkg/cache/redis/trace_test.go | 213 - pkg/cache/redis/util.go | 17 - pkg/cache/redis/util_test.go | 37 - pkg/conf/dsn/README.md | 5 - pkg/conf/dsn/doc.go | 63 - pkg/conf/dsn/dsn.go | 106 - pkg/conf/dsn/dsn_test.go | 79 - pkg/conf/dsn/example_test.go | 31 - pkg/conf/dsn/query.go | 422 -- pkg/conf/dsn/query_test.go | 128 - pkg/conf/env/README.md | 5 - pkg/conf/env/env.go | 76 - pkg/conf/env/env_test.go | 104 - pkg/conf/flagvar/flagvar.go | 18 - pkg/conf/paladin/README.md | 138 - pkg/conf/paladin/apollo/apollo.go | 275 - pkg/conf/paladin/apollo/apollo_test.go | 73 - pkg/conf/paladin/apollo/const.go | 6 - .../apollo/internal/mockserver/mockserver.go | 149 - pkg/conf/paladin/client.go | 49 - pkg/conf/paladin/default.go | 92 - pkg/conf/paladin/driver.go | 9 - pkg/conf/paladin/example_test.go | 147 - pkg/conf/paladin/file.go | 203 - pkg/conf/paladin/file_test.go | 157 - pkg/conf/paladin/helper.go | 76 - pkg/conf/paladin/helper_test.go | 286 - pkg/conf/paladin/map.go | 60 - pkg/conf/paladin/map_test.go | 94 - pkg/conf/paladin/mock.go | 40 - pkg/conf/paladin/mock_test.go | 37 - pkg/conf/paladin/register.go | 55 - pkg/conf/paladin/toml.go | 73 - pkg/conf/paladin/value.go | 185 - pkg/conf/paladin/value_test.go | 206 - pkg/container/group/README.md | 12 - pkg/container/group/example_test.go | 46 - pkg/container/group/group.go | 64 - pkg/container/group/group_test.go | 65 - pkg/container/pool/README.md | 5 - pkg/container/pool/list.go | 226 - pkg/container/pool/list_test.go | 322 - pkg/container/pool/pool.go | 62 - pkg/container/pool/slice.go | 418 -- pkg/container/pool/slice_test.go | 350 - pkg/container/queue/aqm/README.md | 5 - pkg/container/queue/aqm/codel.go | 201 - pkg/container/queue/aqm/codel_test.go | 101 - pkg/database/hbase/README.md | 40 - pkg/database/hbase/config.go | 23 - pkg/database/hbase/hbase.go | 297 - pkg/database/hbase/metrics.go | 65 - pkg/database/hbase/slowlog.go | 24 - pkg/database/hbase/trace.go | 40 - pkg/database/sql/README.md | 9 - pkg/database/sql/metrics.go | 37 - pkg/database/sql/mysql.go | 36 - pkg/database/sql/sql.go | 676 -- pkg/database/sql/sql_test.go | 18 - pkg/database/tidb/README.md | 14 - pkg/database/tidb/discovery.go | 56 - pkg/database/tidb/metrics.go | 37 - pkg/database/tidb/node_proc.go | 82 - pkg/database/tidb/sql.go | 739 -- pkg/database/tidb/tidb.go | 35 - pkg/ecode/common_ecode.go | 20 - pkg/ecode/ecode.go | 116 - pkg/ecode/status.go | 98 - pkg/ecode/status_test.go | 57 - pkg/ecode/types/status.pb.go | 102 - pkg/ecode/types/status.proto | 23 - pkg/log/doc.go | 69 - pkg/log/dsn.go | 48 - pkg/log/field.go | 58 - pkg/log/file.go | 97 - pkg/log/handler.go | 118 - pkg/log/internal/LICENSE.txt | 21 - pkg/log/internal/core/buffer.go | 97 - pkg/log/internal/core/buffer_test.go | 91 - pkg/log/internal/core/bufferpool.go | 29 - pkg/log/internal/core/encoder.go | 187 - pkg/log/internal/core/field.go | 122 - pkg/log/internal/core/json_encoder.go | 424 -- pkg/log/internal/core/pool.go | 52 - pkg/log/internal/core/pool_test.go | 52 - pkg/log/internal/filewriter/filewriter.go | 344 - .../internal/filewriter/filewriter_test.go | 221 - pkg/log/internal/filewriter/option.go | 69 - pkg/log/level.go | 29 - pkg/log/log.go | 325 - pkg/log/log_test.go | 114 - pkg/log/logrus.go | 61 - pkg/log/pattern.go | 164 - pkg/log/pattern_test.go | 35 - pkg/log/stdout.go | 53 - pkg/log/util.go | 69 - pkg/log/util_test.go | 54 - pkg/log/verbose.go | 83 - pkg/naming/README.md | 14 - pkg/naming/discovery/discovery.go | 709 -- pkg/naming/etcd/etcd.go | 324 - pkg/naming/naming.go | 80 - pkg/naming/opt.go | 176 - pkg/naming/opt_test.go | 299 - pkg/naming/zookeeper/zookeeper.go | 396 -- pkg/net/criticality/criticality.go | 57 - pkg/net/http/blademaster/README.md | 5 - pkg/net/http/blademaster/binding/binding.go | 88 - .../http/blademaster/binding/binding_test.go | 341 - .../blademaster/binding/default_validator.go | 50 - .../blademaster/binding/example/test.pb.go | 113 - .../blademaster/binding/example/test.proto | 12 - .../http/blademaster/binding/example_test.go | 36 - pkg/net/http/blademaster/binding/form.go | 55 - .../http/blademaster/binding/form_mapping.go | 276 - pkg/net/http/blademaster/binding/json.go | 22 - pkg/net/http/blademaster/binding/query.go | 19 - pkg/net/http/blademaster/binding/tags.go | 44 - .../http/blademaster/binding/validate_test.go | 209 - pkg/net/http/blademaster/binding/xml.go | 22 - pkg/net/http/blademaster/client.go | 365 - pkg/net/http/blademaster/context.go | 408 -- pkg/net/http/blademaster/cors.go | 249 - pkg/net/http/blademaster/criticality.go | 21 - pkg/net/http/blademaster/csrf.go | 64 - pkg/net/http/blademaster/logger.go | 70 - pkg/net/http/blademaster/metadata.go | 123 - pkg/net/http/blademaster/metrics.go | 48 - pkg/net/http/blademaster/perf.go | 63 - pkg/net/http/blademaster/prometheus.go | 12 - pkg/net/http/blademaster/ratelimit.go | 53 - pkg/net/http/blademaster/recovery.go | 32 - pkg/net/http/blademaster/render/data.go | 30 - pkg/net/http/blademaster/render/json.go | 58 - pkg/net/http/blademaster/render/protobuf.go | 38 - pkg/net/http/blademaster/render/redirect.go | 26 - pkg/net/http/blademaster/render/render.go | 30 - pkg/net/http/blademaster/render/render.pb.go | 89 - pkg/net/http/blademaster/render/render.proto | 14 - pkg/net/http/blademaster/render/string.go | 40 - pkg/net/http/blademaster/render/xml.go | 31 - pkg/net/http/blademaster/routergroup.go | 191 - pkg/net/http/blademaster/server.go | 517 -- pkg/net/http/blademaster/server_test.go | 138 - pkg/net/http/blademaster/trace.go | 229 - pkg/net/http/blademaster/tree.go | 625 -- pkg/net/http/blademaster/utils.go | 159 - pkg/net/ip/ip.go | 74 - pkg/net/metadata/README.md | 5 - pkg/net/metadata/key.go | 67 - pkg/net/metadata/metadata.go | 156 - pkg/net/metadata/metadata_test.go | 148 - pkg/net/netutil/backoff.go | 72 - pkg/net/netutil/breaker/README.md | 20 - pkg/net/netutil/breaker/breaker.go | 164 - pkg/net/netutil/breaker/breaker_test.go | 94 - pkg/net/netutil/breaker/example_test.go | 60 - pkg/net/netutil/breaker/sre_breaker.go | 100 - pkg/net/netutil/breaker/sre_breaker_test.go | 177 - pkg/net/rpc/warden/README.md | 13 - pkg/net/rpc/warden/balancer/p2c/README.md | 5 - pkg/net/rpc/warden/balancer/p2c/p2c.go | 293 - pkg/net/rpc/warden/balancer/p2c/p2c_test.go | 345 - pkg/net/rpc/warden/balancer/wrr/README.md | 5 - pkg/net/rpc/warden/balancer/wrr/wrr.go | 302 - pkg/net/rpc/warden/balancer/wrr/wrr_test.go | 190 - pkg/net/rpc/warden/client.go | 381 -- pkg/net/rpc/warden/client_test.go | 34 - pkg/net/rpc/warden/exapmle_test.go | 89 - .../internal/benchmark/bench/client/client.go | 186 - .../benchmark/bench/proto/hello.pb.go | 1686 ----- .../benchmark/bench/proto/hello.proto | 60 - .../internal/benchmark/bench/server/server.go | 103 - .../internal/benchmark/helloworld/client.sh | 15 - .../helloworld/client/greeter_client.go | 83 - .../helloworld/server/greeter_server.go | 49 - .../rpc/warden/internal/encoding/json/json.go | 53 - .../warden/internal/examples/client/client.go | 31 - .../internal/examples/grpcDebug/client.go | 191 - .../internal/examples/grpcDebug/data.json | 1 - .../warden/internal/examples/server/main.go | 104 - .../rpc/warden/internal/metadata/metadata.go | 11 - .../internal/proto/testproto/hello.pb.go | 642 -- .../internal/proto/testproto/hello.proto | 33 - pkg/net/rpc/warden/internal/status/status.go | 120 - .../rpc/warden/internal/status/status_test.go | 125 - pkg/net/rpc/warden/logging.go | 174 - pkg/net/rpc/warden/logging_test.go | 294 - pkg/net/rpc/warden/metrics.go | 41 - pkg/net/rpc/warden/ratelimiter/ratelimiter.go | 65 - pkg/net/rpc/warden/recovery.go | 61 - pkg/net/rpc/warden/resolver/README.md | 5 - pkg/net/rpc/warden/resolver/direct/README.md | 6 - pkg/net/rpc/warden/resolver/direct/direct.go | 78 - .../resolver/direct/test/direct_test.go | 85 - pkg/net/rpc/warden/resolver/resolver.go | 162 - .../rpc/warden/resolver/test/mockdiscovery.go | 87 - .../rpc/warden/resolver/test/resovler_test.go | 314 - pkg/net/rpc/warden/resolver/util.go | 16 - pkg/net/rpc/warden/server.go | 362 - pkg/net/rpc/warden/server_test.go | 605 -- pkg/net/rpc/warden/stats.go | 25 - pkg/net/rpc/warden/validate.go | 37 - pkg/net/trace/README.md | 20 - pkg/net/trace/config.go | 75 - pkg/net/trace/config_test.go | 33 - pkg/net/trace/const.go | 7 - pkg/net/trace/context.go | 110 - pkg/net/trace/context_test.go | 26 - pkg/net/trace/dapper.go | 189 - pkg/net/trace/dapper_test.go | 136 - pkg/net/trace/jaeger/config.go | 33 - pkg/net/trace/jaeger/http_transport.go | 314 - pkg/net/trace/jaeger/jaeger.go | 49 - pkg/net/trace/jaeger/jaeger_test.go | 53 - pkg/net/trace/jaeger/reference.go | 9 - pkg/net/trace/jaeger/span.go | 345 - pkg/net/trace/jaeger/span_context.go | 369 - pkg/net/trace/marshal.go | 106 - pkg/net/trace/marshal_test.go | 18 - pkg/net/trace/mocktrace/mocktrace.go | 84 - pkg/net/trace/mocktrace/mocktrace_test.go | 24 - pkg/net/trace/noop.go | 47 - pkg/net/trace/option.go | 17 - pkg/net/trace/propagation.go | 177 - pkg/net/trace/proto/span.pb.go | 557 -- pkg/net/trace/proto/span.proto | 77 - pkg/net/trace/report.go | 138 - pkg/net/trace/report_test.go | 88 - pkg/net/trace/sample.go | 67 - pkg/net/trace/sample_test.go | 35 - pkg/net/trace/span.go | 141 - pkg/net/trace/span_test.go | 108 - pkg/net/trace/tag.go | 182 - pkg/net/trace/tag_test.go | 1 - pkg/net/trace/tracer.go | 94 - pkg/net/trace/util.go | 59 - pkg/net/trace/util_test.go | 21 - pkg/net/trace/zipkin/config.go | 30 - pkg/net/trace/zipkin/zipkin.go | 99 - pkg/net/trace/zipkin/zipkin_test.go | 53 - pkg/ratelimit/README.md | 14 - pkg/ratelimit/bbr/bbr.go | 284 - pkg/ratelimit/bbr/bbr_test.go | 298 - pkg/ratelimit/limiter.go | 40 - pkg/stat/README.md | 5 - pkg/stat/metric/counter.go | 84 - pkg/stat/metric/counter_test.go | 47 - pkg/stat/metric/gauge.go | 94 - pkg/stat/metric/gauge_test.go | 23 - pkg/stat/metric/histogram.go | 50 - pkg/stat/metric/iterator.go | 26 - pkg/stat/metric/metric.go | 104 - pkg/stat/metric/point_gauge.go | 61 - pkg/stat/metric/point_gauge_test.go | 55 - pkg/stat/metric/point_policy.go | 57 - pkg/stat/metric/reduce.go | 77 - pkg/stat/metric/reduce_test.go | 17 - pkg/stat/metric/rolling_counter.go | 75 - pkg/stat/metric/rolling_counter_test.go | 156 - pkg/stat/metric/rolling_gauge.go | 62 - pkg/stat/metric/rolling_gauge_test.go | 192 - pkg/stat/metric/rolling_policy.go | 100 - pkg/stat/metric/rolling_policy_test.go | 67 - pkg/stat/metric/window.go | 107 - pkg/stat/metric/window_test.go | 68 - pkg/stat/sys/cpu/README.md | 7 - pkg/stat/sys/cpu/cgroup.go | 126 - pkg/stat/sys/cpu/cgroupCPU.go | 250 - pkg/stat/sys/cpu/cgroup_test.go | 11 - pkg/stat/sys/cpu/cpu.go | 68 - pkg/stat/sys/cpu/cpu_test.go | 22 - pkg/stat/sys/cpu/psutilCPU.go | 45 - pkg/stat/sys/cpu/stat_test.go | 20 - pkg/stat/sys/cpu/util.go | 121 - pkg/str/str.go | 55 - pkg/str/str_test.go | 60 - pkg/sync/errgroup/README.md | 3 - pkg/sync/errgroup/doc.go | 47 - pkg/sync/errgroup/errgroup.go | 119 - pkg/sync/errgroup/errgroup_test.go | 266 - pkg/sync/errgroup/example_test.go | 47 - pkg/sync/pipeline/CHANGELOG.md | 9 - pkg/sync/pipeline/README.md | 3 - pkg/sync/pipeline/fanout/CHANGELOG.md | 6 - pkg/sync/pipeline/fanout/README.md | 14 - pkg/sync/pipeline/fanout/example_test.go | 22 - pkg/sync/pipeline/fanout/fanout.go | 150 - pkg/sync/pipeline/fanout/fanout_test.go | 30 - pkg/sync/pipeline/fanout/metrics.go | 27 - pkg/sync/pipeline/pipeline.go | 223 - pkg/sync/pipeline/pipeline_test.go | 131 - pkg/testing/lich/README.md | 4 - pkg/testing/lich/composer.go | 129 - pkg/testing/lich/healthcheck.go | 85 - pkg/testing/lich/model.go | 88 - pkg/time/README.md | 5 - pkg/time/time.go | 59 - pkg/time/time_test.go | 60 - registry/registry.go | 41 + third_party/CHANGELOG.md | 7 - third_party/README.md | 1 + third_party/github.com/gogo/protobuf/AUTHORS | 15 - .../github.com/gogo/protobuf/CONTRIBUTORS | 23 - third_party/github.com/gogo/protobuf/LICENSE | 35 - .../gogo/protobuf/gogoproto/gogo.proto | 144 - third_party/google/api/README.md | 5 - third_party/google/api/auth.proto | 184 - third_party/google/api/backend.proto | 127 - third_party/google/api/billing.proto | 67 - third_party/google/api/client.proto | 100 - third_party/google/api/config_change.proto | 85 - third_party/google/api/consumer.proto | 82 - third_party/google/api/context.proto | 90 - third_party/google/api/control.proto | 33 - third_party/google/api/distribution.proto | 212 - third_party/google/api/documentation.proto | 157 - third_party/google/api/endpoint.proto | 70 - .../experimental/authorization_config.proto | 40 - .../api/experimental/experimental.proto | 34 - third_party/google/api/expr/artman_cel.yaml | 37 - third_party/google/api/expr/cel.yaml | 61 - .../google/api/expr/v1alpha1/cel_gapic.yaml | 246 - .../api/expr/v1alpha1/cel_service.proto | 44 - .../google/api/expr/v1alpha1/checked.proto | 336 - .../expr/v1alpha1/conformance_service.proto | 165 - .../google/api/expr/v1alpha1/eval.proto | 119 - .../google/api/expr/v1alpha1/explain.proto | 54 - .../google/api/expr/v1alpha1/syntax.proto | 322 - .../google/api/expr/v1alpha1/value.proto | 116 - .../google/api/expr/v1beta1/decl.proto | 84 - .../google/api/expr/v1beta1/eval.proto | 125 - .../google/api/expr/v1beta1/expr.proto | 269 - .../google/api/expr/v1beta1/source.proto | 62 - .../google/api/expr/v1beta1/value.proto | 114 - third_party/google/api/field_behavior.proto | 79 - third_party/google/api/http.proto | 3 +- third_party/google/api/httpbody.proto | 3 +- third_party/google/api/label.proto | 49 - third_party/google/api/launch_stage.proto | 67 - third_party/google/api/log.proto | 55 - third_party/google/api/logging.proto | 81 - third_party/google/api/metric.proto | 215 - .../google/api/monitored_resource.proto | 115 - third_party/google/api/monitoring.proto | 91 - third_party/google/api/quota.proto | 187 - third_party/google/api/resource.proto | 199 - third_party/google/api/service.proto | 180 - third_party/google/api/serviceconfig.yaml | 24 - .../google/api/servicecontrol/README.md | 126 - .../api/servicecontrol/v1/check_error.proto | 98 - .../api/servicecontrol/v1/distribution.proto | 158 - .../api/servicecontrol/v1/log_entry.proto | 66 - .../api/servicecontrol/v1/metric_value.proto | 78 - .../api/servicecontrol/v1/operation.proto | 113 - .../servicecontrol/v1/quota_controller.proto | 206 - .../v1/service_controller.proto | 198 - .../google/api/servicemanagement/README.md | 102 - .../artman_servicemanagement_v1.yaml | 34 - .../servicemanagement_v1.yaml | 233 - .../api/servicemanagement/v1/resources.proto | 299 - .../v1/servicemanagement_gapic.yaml | 300 - .../servicemanagement/v1/servicemanager.proto | 503 -- third_party/google/api/source_info.proto | 32 - third_party/google/api/system_parameter.proto | 96 - third_party/google/api/usage.proto | 90 - tool/kratos-gen-bts/README.md | 48 - tool/kratos-gen-bts/header_template.go | 33 - tool/kratos-gen-bts/main.go | 507 -- tool/kratos-gen-bts/multi_template.go | 130 - tool/kratos-gen-bts/none_template.go | 69 - tool/kratos-gen-bts/single_template.go | 90 - tool/kratos-gen-bts/testdata/dao.bts.go | 283 - tool/kratos-gen-bts/testdata/dao.go | 37 - tool/kratos-gen-bts/testdata/multi.go | 48 - tool/kratos-gen-bts/testdata/multi_test.go | 67 - tool/kratos-gen-bts/testdata/none.go | 30 - tool/kratos-gen-bts/testdata/none_test.go | 50 - tool/kratos-gen-bts/testdata/single.go | 48 - tool/kratos-gen-bts/testdata/single_test.go | 50 - tool/kratos-gen-mc/README.md | 45 - tool/kratos-gen-mc/header_template.go | 29 - tool/kratos-gen-mc/main.go | 570 -- tool/kratos-gen-mc/multi_template.go | 205 - tool/kratos-gen-mc/none_template.go | 104 - tool/kratos-gen-mc/single_template.go | 103 - tool/kratos-gen-mc/testdata/dao.go | 93 - tool/kratos-gen-mc/testdata/dao_test.go | 116 - tool/kratos-gen-mc/testdata/mc.cache.go | 305 - tool/kratos-gen-mc/testdata/model.pb.go | 328 - tool/kratos-gen-mc/testdata/model.proto | 14 - tool/kratos-gen-project/main-packr.go | 8 - tool/kratos-gen-project/main.go | 75 - tool/kratos-gen-project/new.go | 61 - .../kratos-gen-project/packrd/packed-packr.go | 230 - tool/kratos-gen-project/project.go | 96 - .../templates/all/CHANGELOG.md | 4 - tool/kratos-gen-project/templates/all/OWNERS | 2 - .../templates/all/README.md | 4 - .../templates/all/api/api.proto | 34 - .../templates/all/api/client.go.tmpl | 25 - .../templates/all/cmd/main.go.tmpl | 41 - .../templates/all/configs/application.toml | 3 - .../templates/all/configs/db.toml | 10 - .../templates/all/configs/grpc.toml | 3 - .../templates/all/configs/http.toml | 3 - .../templates/all/configs/memcache.toml | 10 - .../templates/all/configs/redis.toml | 10 - .../templates/all/go.mod.tmpl | 12 - .../templates/all/internal/dao/dao.go.tmpl | 69 - .../all/internal/dao/dao_test.go.tmpl | 40 - .../templates/all/internal/dao/db.go.tmpl | 30 - .../templates/all/internal/dao/mc.go.tmpl | 48 - .../templates/all/internal/dao/redis.go.tmpl | 32 - .../templates/all/internal/dao/wire.go.tmpl | 13 - .../templates/all/internal/di/app.go.tmpl | 38 - .../templates/all/internal/di/wire.go.tmpl | 18 - .../all/internal/model/model.go.tmpl | 12 - .../all/internal/server/grpc/server.go.tmpl | 26 - .../all/internal/server/http/server.go.tmpl | 56 - .../all/internal/service/service.go.tmpl | 57 - .../templates/all/test/0_db.sql | 11 - .../templates/all/test/1_data.sql | 3 - .../templates/all/test/application.toml | 3 - .../templates/all/test/db.toml | 8 - .../templates/all/test/docker-compose.yaml | 40 - .../templates/all/test/grpc.toml | 3 - .../templates/all/test/http.toml | 3 - .../templates/all/test/memcache.toml | 10 - .../templates/all/test/redis.toml | 10 - .../templates/grpc/CHANGELOG.md | 4 - tool/kratos-gen-project/templates/grpc/OWNERS | 2 - .../templates/grpc/README.md | 4 - .../templates/grpc/api/api.proto | 34 - .../templates/grpc/api/client.go.tmpl | 25 - .../templates/grpc/cmd/main.go.tmpl | 41 - .../templates/grpc/configs/application.toml | 3 - .../templates/grpc/configs/db.toml | 10 - .../templates/grpc/configs/grpc.toml | 3 - .../templates/grpc/configs/memcache.toml | 10 - .../templates/grpc/configs/redis.toml | 10 - .../templates/grpc/go.mod.tmpl | 12 - .../templates/grpc/internal/dao/dao.go.tmpl | 69 - .../grpc/internal/dao/dao_test.go.tmpl | 40 - .../templates/grpc/internal/dao/db.go.tmpl | 30 - .../templates/grpc/internal/dao/mc.go.tmpl | 48 - .../templates/grpc/internal/dao/redis.go.tmpl | 32 - .../templates/grpc/internal/dao/wire.go.tmpl | 13 - .../templates/grpc/internal/di/app.go.tmpl | 32 - .../templates/grpc/internal/di/wire.go.tmpl | 17 - .../grpc/internal/model/model.go.tmpl | 12 - .../grpc/internal/server/grpc/server.go.tmpl | 26 - .../grpc/internal/service/service.go.tmpl | 57 - .../templates/grpc/test/0_db.sql | 11 - .../templates/grpc/test/1_data.sql | 3 - .../templates/grpc/test/application.toml | 3 - .../templates/grpc/test/db.toml | 8 - .../templates/grpc/test/docker-compose.yaml | 40 - .../templates/grpc/test/grpc.toml | 3 - .../templates/grpc/test/memcache.toml | 10 - .../templates/grpc/test/redis.toml | 10 - .../templates/http/CHANGELOG.md | 4 - tool/kratos-gen-project/templates/http/OWNERS | 2 - .../templates/http/README.md | 4 - .../templates/http/api/api.proto | 34 - .../templates/http/api/client.go.tmpl | 25 - .../templates/http/cmd/main.go.tmpl | 41 - .../templates/http/configs/application.toml | 3 - .../templates/http/configs/db.toml | 10 - .../templates/http/configs/http.toml | 3 - .../templates/http/configs/memcache.toml | 10 - .../templates/http/configs/redis.toml | 10 - .../templates/http/go.mod.tmpl | 12 - .../templates/http/internal/dao/dao.go.tmpl | 69 - .../http/internal/dao/dao_test.go.tmpl | 40 - .../templates/http/internal/dao/db.go.tmpl | 30 - .../templates/http/internal/dao/mc.go.tmpl | 48 - .../templates/http/internal/dao/redis.go.tmpl | 32 - .../templates/http/internal/dao/wire.go.tmpl | 13 - .../templates/http/internal/di/app.go.tmpl | 32 - .../templates/http/internal/di/wire.go.tmpl | 17 - .../http/internal/model/model.go.tmpl | 12 - .../http/internal/server/http/server.go.tmpl | 56 - .../http/internal/service/service.go.tmpl | 57 - .../templates/http/test/0_db.sql | 11 - .../templates/http/test/1_data.sql | 3 - .../templates/http/test/application.toml | 3 - .../templates/http/test/db.toml | 8 - .../templates/http/test/docker-compose.yaml | 40 - .../templates/http/test/http.toml | 3 - .../templates/http/test/memcache.toml | 10 - .../templates/http/test/redis.toml | 10 - .../testdata/test_in_gomod.sh | 37 - .../testdata/test_not_in_gomod.sh | 41 - tool/kratos-protoc/bm.go | 23 - tool/kratos-protoc/ecode.go | 28 - tool/kratos-protoc/grpc.go | 28 - tool/kratos-protoc/main.go | 42 - tool/kratos-protoc/protoc.go | 181 - tool/kratos-protoc/swagger.go | 28 - tool/kratos/README.MD | 14 - tool/kratos/build.go | 47 - tool/kratos/env.go | 78 - tool/kratos/main.go | 65 - tool/kratos/run.go | 28 - tool/kratos/tool.go | 284 - tool/kratos/tool_index.go | 88 - tool/kratos/version.go | 44 - tool/pkg/common.go | 151 - .../pkg/extensions/gogoproto/gogo.pb.go | 818 --- .../pkg/extensions/gogoproto/gogo.pb.golden | 45 - .../pkg/extensions/gogoproto/gogo.proto | 136 - .../extensions/google/api/annotations.proto | 31 - .../pkg/extensions/google/api/http.proto | 318 - tool/protobuf/pkg/gen/main.go | 92 - tool/protobuf/pkg/generator/command_line.go | 71 - tool/protobuf/pkg/generator/generator.go | 318 - tool/protobuf/pkg/generator/helper.go | 136 - tool/protobuf/pkg/generator/http.go | 146 - tool/protobuf/pkg/naming/go_naming.go | 27 - tool/protobuf/pkg/naming/naming.go | 113 - tool/protobuf/pkg/project/project.go | 121 - tool/protobuf/pkg/tag/ext_tags.go | 21 - tool/protobuf/pkg/tag/tags.go | 55 - tool/protobuf/pkg/typemap/typemap.go | 277 - tool/protobuf/pkg/utils/stringutils.go | 97 - tool/protobuf/pkg/utils/utils.go | 29 - .../protoc-gen-bm/generator/generator.go | 337 - .../protoc-gen-bm/generator/generator_test.go | 27 - tool/protobuf/protoc-gen-bm/main.go | 23 - .../protobuf/protoc-gen-bswagger/generator.go | 342 - tool/protobuf/protoc-gen-bswagger/main.go | 22 - tool/protobuf/protoc-gen-bswagger/types.go | 218 - .../protoc-gen-ecode/generator/generator.go | 118 - .../generator/generator_test.go | 27 - tool/protobuf/protoc-gen-ecode/main.go | 23 - tool/testcli/README.md | 154 - tool/testcli/docker-compose.yaml | 26 - tool/testcli/main.go | 50 - tool/testgen/README.md | 52 - tool/testgen/gen.go | 419 -- tool/testgen/main.go | 57 - tool/testgen/parser.go | 193 - tool/testgen/templete.go | 41 - tool/testgen/utils.go | 42 - transport/grpc/client.go | 115 + transport/grpc/context.go | 43 + transport/grpc/resolver/direct/builder.go | 35 + transport/grpc/resolver/direct/resolver.go | 15 + transport/grpc/resolver/discovery/builder.go | 54 + transport/grpc/resolver/discovery/resolver.go | 74 + transport/grpc/server.go | 159 + transport/grpc/server_test.go | 40 + transport/http/bind.go | 287 + transport/http/client.go | 136 + transport/http/context.go | 50 + transport/http/default.go | 81 + transport/http/errors.go | 59 + transport/http/route.go | 55 + transport/http/server.go | 162 + transport/http/server_test.go | 79 + transport/http/service.go | 51 + transport/http/service_test.go | 98 + transport/transport.go | 34 + 763 files changed, 7530 insertions(+), 77187 deletions(-) create mode 100644 .github/FUNDING.yml delete mode 100644 .github/workflows/go.yml delete mode 100644 .golangci.yml delete mode 100644 .travis.yml create mode 100644 api/README.md create mode 100644 api/kratos/api/annotations.pb.go create mode 100644 api/kratos/api/annotations.proto create mode 100644 app.go create mode 100644 app_test.go create mode 100644 cmd/kratos/go.mod create mode 100644 cmd/kratos/go.sum create mode 100644 cmd/kratos/internal/base/mod.go create mode 100644 cmd/kratos/internal/base/path.go create mode 100644 cmd/kratos/internal/base/repo.go create mode 100644 cmd/kratos/internal/base/repo_test.go create mode 100644 cmd/kratos/internal/new/new.go create mode 100644 cmd/kratos/internal/new/project.go create mode 100644 cmd/kratos/internal/proto/add/add.go create mode 100644 cmd/kratos/internal/proto/add/proto.go create mode 100644 cmd/kratos/internal/proto/add/template.go create mode 100644 cmd/kratos/internal/proto/proto.go create mode 100644 cmd/kratos/internal/proto/service/service.go create mode 100644 cmd/kratos/internal/proto/service/template.go create mode 100644 cmd/kratos/internal/proto/source/source.go create mode 100644 cmd/kratos/main.go create mode 100644 cmd/kratos/version.go create mode 100644 cmd/protoc-gen-go-errors/errors.go create mode 100644 cmd/protoc-gen-go-errors/go.mod create mode 100644 cmd/protoc-gen-go-errors/go.sum create mode 100644 cmd/protoc-gen-go-errors/main.go create mode 100644 cmd/protoc-gen-go-errors/template.go create mode 100644 cmd/protoc-gen-go-http/go.mod create mode 100644 cmd/protoc-gen-go-http/go.sum create mode 100644 cmd/protoc-gen-go-http/http.go create mode 100644 cmd/protoc-gen-go-http/internal/testproto/echo_service.pb.go create mode 100644 cmd/protoc-gen-go-http/internal/testproto/echo_service.proto create mode 100644 cmd/protoc-gen-go-http/internal/testproto/echo_service_grpc.pb.go create mode 100644 cmd/protoc-gen-go-http/internal/testproto/echo_service_http.pb.go create mode 100644 cmd/protoc-gen-go-http/main.go create mode 100644 cmd/protoc-gen-go-http/template.go create mode 100644 config/README.md create mode 100644 config/config.go create mode 100644 config/file/file.go create mode 100644 config/file/file_test.go create mode 100644 config/file/watcher.go create mode 100644 config/options.go create mode 100644 config/reader.go create mode 100644 config/source.go create mode 100644 config/value.go delete mode 100644 docs/.nojekyll delete mode 100644 docs/CNAME delete mode 100644 docs/FAQ.md delete mode 100644 docs/_sidebar.md delete mode 100644 docs/blademaster-mid.md delete mode 100644 docs/blademaster-mod.md delete mode 100644 docs/blademaster-pb.md delete mode 100644 docs/blademaster-quickstart.md delete mode 100644 docs/blademaster.md delete mode 100644 docs/breaker.md delete mode 100644 docs/cache-mc.md delete mode 100644 docs/cache-redis.md delete mode 100644 docs/cache.md delete mode 100644 docs/config-paladin.md delete mode 100644 docs/config.md delete mode 100644 docs/database-hbase.md delete mode 100644 docs/database-mysql-orm.md delete mode 100644 docs/database-mysql.md delete mode 100644 docs/database-tidb.md delete mode 100644 docs/database.md create mode 100644 docs/design/kratos-v2.md delete mode 100644 docs/ecode.md create mode 100644 docs/images/alipay.png rename docs/{img/kratos3.png => images/kratos.png} (100%) delete mode 100644 docs/img/bm-arch-2-2.png delete mode 100644 docs/img/bm-arch-2-3.png delete mode 100644 docs/img/bm-handlers.png delete mode 100644 docs/img/kratos-log.jpg delete mode 100644 docs/img/kratos.png delete mode 100644 docs/img/kratos2.png delete mode 100644 docs/img/ratelimit-benchmark-up-1.png delete mode 100644 docs/img/ratelimit-rolling-window.png delete mode 100644 docs/img/zipkin.jpg delete mode 100644 docs/index.html delete mode 100644 docs/install.md delete mode 100644 docs/kratos-genbts.md delete mode 100644 docs/kratos-genmc.md delete mode 100644 docs/kratos-protoc.md delete mode 100644 docs/kratos-swagger.md delete mode 100644 docs/kratos-tool.md delete mode 100644 docs/log-agent.md delete mode 100644 docs/logger.md delete mode 100644 docs/protoc.md delete mode 100644 docs/quickstart.md delete mode 100644 docs/ratelimit.md delete mode 100644 docs/trace.md delete mode 100644 docs/ut-support.md delete mode 100644 docs/ut-testcli.md delete mode 100644 docs/ut-testgen.md delete mode 100644 docs/ut.md delete mode 100644 docs/warden-balancer.md delete mode 100644 docs/warden-mid.md delete mode 100644 docs/warden-pb.md delete mode 100644 docs/warden-quickstart.md delete mode 100644 docs/warden-resolver.md delete mode 100644 docs/warden.md create mode 100644 encoding/encoding.go create mode 100644 encoding/json/json.go create mode 100644 encoding/proto/proto.go create mode 100644 errors/codes.go create mode 100644 errors/errors.go create mode 100644 errors/errors.pb.go create mode 100644 errors/errors.proto create mode 100644 errors/errors_test.go delete mode 100644 example/blademaster/middleware/auth/README.md delete mode 100644 example/blademaster/middleware/auth/auth.go delete mode 100644 example/blademaster/middleware/auth/example_test.go delete mode 100644 example/protobuf/api.bm.go delete mode 100644 example/protobuf/api.ecode.go delete mode 100644 example/protobuf/api.pb.go delete mode 100644 example/protobuf/api.proto delete mode 100644 example/protobuf/api.swagger.json delete mode 100644 example/protobuf/gen.sh create mode 100644 internal/README.md create mode 100644 internal/host/host.go create mode 100644 internal/host/host_test.go create mode 100644 log/README.md create mode 100644 log/helper.go create mode 100644 log/helper_test.go create mode 100644 log/level.go create mode 100644 log/log.go create mode 100644 log/log_test.go create mode 100644 log/nop.go create mode 100644 log/std.go create mode 100644 log/std_test.go create mode 100644 metrics/README.md create mode 100644 metrics/metrics.go create mode 100644 middleware/logging/logging.go create mode 100644 middleware/middleware.go create mode 100644 middleware/recovery/recovery.go create mode 100644 middleware/status/status.go create mode 100644 middleware/status/status_test.go create mode 100644 middleware/tracing/tracing.go delete mode 100644 misc/stat/dashboard/README.md delete mode 100644 misc/stat/dashboard/prometheus.json create mode 100644 options.go delete mode 100644 pkg/cache/memcache/ascii_conn.go delete mode 100644 pkg/cache/memcache/ascii_conn_test.go delete mode 100644 pkg/cache/memcache/conn.go delete mode 100644 pkg/cache/memcache/conn_test.go delete mode 100644 pkg/cache/memcache/encoding.go delete mode 100644 pkg/cache/memcache/encoding_test.go delete mode 100644 pkg/cache/memcache/errors.go delete mode 100644 pkg/cache/memcache/example_test.go delete mode 100644 pkg/cache/memcache/main_test.go delete mode 100644 pkg/cache/memcache/memcache.go delete mode 100644 pkg/cache/memcache/memcache_test.go delete mode 100644 pkg/cache/memcache/metrics.go delete mode 100644 pkg/cache/memcache/pool_conn.go delete mode 100644 pkg/cache/memcache/pool_conn_test.go delete mode 100755 pkg/cache/memcache/test/docker-compose.yaml delete mode 100644 pkg/cache/memcache/test/test.pb.go delete mode 100644 pkg/cache/memcache/test/test.proto delete mode 100644 pkg/cache/memcache/trace_conn.go delete mode 100644 pkg/cache/memcache/util.go delete mode 100644 pkg/cache/memcache/util_test.go delete mode 100644 pkg/cache/metrics.go delete mode 100644 pkg/cache/redis/README.md delete mode 100644 pkg/cache/redis/commandinfo.go delete mode 100644 pkg/cache/redis/commandinfo_test.go delete mode 100644 pkg/cache/redis/conn.go delete mode 100644 pkg/cache/redis/conn_test.go delete mode 100644 pkg/cache/redis/doc.go delete mode 100644 pkg/cache/redis/errors.go delete mode 100644 pkg/cache/redis/log.go delete mode 100644 pkg/cache/redis/main_test.go delete mode 100644 pkg/cache/redis/metrics.go delete mode 100644 pkg/cache/redis/mock.go delete mode 100644 pkg/cache/redis/pipeline.go delete mode 100644 pkg/cache/redis/pipeline_test.go delete mode 100644 pkg/cache/redis/pool.go delete mode 100644 pkg/cache/redis/pool_test.go delete mode 100644 pkg/cache/redis/pubsub.go delete mode 100644 pkg/cache/redis/pubsub_test.go delete mode 100644 pkg/cache/redis/redis.go delete mode 100644 pkg/cache/redis/redis_test.go delete mode 100644 pkg/cache/redis/reply.go delete mode 100644 pkg/cache/redis/reply_test.go delete mode 100644 pkg/cache/redis/scan.go delete mode 100644 pkg/cache/redis/scan_test.go delete mode 100644 pkg/cache/redis/script.go delete mode 100644 pkg/cache/redis/script_test.go delete mode 100644 pkg/cache/redis/test/docker-compose.yaml delete mode 100644 pkg/cache/redis/trace.go delete mode 100644 pkg/cache/redis/trace_test.go delete mode 100644 pkg/cache/redis/util.go delete mode 100644 pkg/cache/redis/util_test.go delete mode 100644 pkg/conf/dsn/README.md delete mode 100644 pkg/conf/dsn/doc.go delete mode 100644 pkg/conf/dsn/dsn.go delete mode 100644 pkg/conf/dsn/dsn_test.go delete mode 100644 pkg/conf/dsn/example_test.go delete mode 100644 pkg/conf/dsn/query.go delete mode 100644 pkg/conf/dsn/query_test.go delete mode 100644 pkg/conf/env/README.md delete mode 100644 pkg/conf/env/env.go delete mode 100644 pkg/conf/env/env_test.go delete mode 100644 pkg/conf/flagvar/flagvar.go delete mode 100644 pkg/conf/paladin/README.md delete mode 100644 pkg/conf/paladin/apollo/apollo.go delete mode 100644 pkg/conf/paladin/apollo/apollo_test.go delete mode 100644 pkg/conf/paladin/apollo/const.go delete mode 100644 pkg/conf/paladin/apollo/internal/mockserver/mockserver.go delete mode 100644 pkg/conf/paladin/client.go delete mode 100644 pkg/conf/paladin/default.go delete mode 100644 pkg/conf/paladin/driver.go delete mode 100644 pkg/conf/paladin/example_test.go delete mode 100644 pkg/conf/paladin/file.go delete mode 100644 pkg/conf/paladin/file_test.go delete mode 100644 pkg/conf/paladin/helper.go delete mode 100644 pkg/conf/paladin/helper_test.go delete mode 100644 pkg/conf/paladin/map.go delete mode 100644 pkg/conf/paladin/map_test.go delete mode 100644 pkg/conf/paladin/mock.go delete mode 100644 pkg/conf/paladin/mock_test.go delete mode 100644 pkg/conf/paladin/register.go delete mode 100644 pkg/conf/paladin/toml.go delete mode 100644 pkg/conf/paladin/value.go delete mode 100644 pkg/conf/paladin/value_test.go delete mode 100644 pkg/container/group/README.md delete mode 100644 pkg/container/group/example_test.go delete mode 100644 pkg/container/group/group.go delete mode 100644 pkg/container/group/group_test.go delete mode 100644 pkg/container/pool/README.md delete mode 100644 pkg/container/pool/list.go delete mode 100644 pkg/container/pool/list_test.go delete mode 100644 pkg/container/pool/pool.go delete mode 100644 pkg/container/pool/slice.go delete mode 100644 pkg/container/pool/slice_test.go delete mode 100644 pkg/container/queue/aqm/README.md delete mode 100644 pkg/container/queue/aqm/codel.go delete mode 100644 pkg/container/queue/aqm/codel_test.go delete mode 100644 pkg/database/hbase/README.md delete mode 100644 pkg/database/hbase/config.go delete mode 100644 pkg/database/hbase/hbase.go delete mode 100644 pkg/database/hbase/metrics.go delete mode 100644 pkg/database/hbase/slowlog.go delete mode 100644 pkg/database/hbase/trace.go delete mode 100644 pkg/database/sql/README.md delete mode 100644 pkg/database/sql/metrics.go delete mode 100644 pkg/database/sql/mysql.go delete mode 100644 pkg/database/sql/sql.go delete mode 100644 pkg/database/sql/sql_test.go delete mode 100644 pkg/database/tidb/README.md delete mode 100644 pkg/database/tidb/discovery.go delete mode 100644 pkg/database/tidb/metrics.go delete mode 100644 pkg/database/tidb/node_proc.go delete mode 100644 pkg/database/tidb/sql.go delete mode 100644 pkg/database/tidb/tidb.go delete mode 100644 pkg/ecode/common_ecode.go delete mode 100644 pkg/ecode/ecode.go delete mode 100644 pkg/ecode/status.go delete mode 100644 pkg/ecode/status_test.go delete mode 100644 pkg/ecode/types/status.pb.go delete mode 100644 pkg/ecode/types/status.proto delete mode 100644 pkg/log/doc.go delete mode 100644 pkg/log/dsn.go delete mode 100644 pkg/log/field.go delete mode 100644 pkg/log/file.go delete mode 100644 pkg/log/handler.go delete mode 100644 pkg/log/internal/LICENSE.txt delete mode 100644 pkg/log/internal/core/buffer.go delete mode 100644 pkg/log/internal/core/buffer_test.go delete mode 100644 pkg/log/internal/core/bufferpool.go delete mode 100644 pkg/log/internal/core/encoder.go delete mode 100644 pkg/log/internal/core/field.go delete mode 100644 pkg/log/internal/core/json_encoder.go delete mode 100644 pkg/log/internal/core/pool.go delete mode 100644 pkg/log/internal/core/pool_test.go delete mode 100644 pkg/log/internal/filewriter/filewriter.go delete mode 100644 pkg/log/internal/filewriter/filewriter_test.go delete mode 100644 pkg/log/internal/filewriter/option.go delete mode 100644 pkg/log/level.go delete mode 100644 pkg/log/log.go delete mode 100644 pkg/log/log_test.go delete mode 100644 pkg/log/logrus.go delete mode 100644 pkg/log/pattern.go delete mode 100644 pkg/log/pattern_test.go delete mode 100644 pkg/log/stdout.go delete mode 100644 pkg/log/util.go delete mode 100644 pkg/log/util_test.go delete mode 100644 pkg/log/verbose.go delete mode 100644 pkg/naming/README.md delete mode 100644 pkg/naming/discovery/discovery.go delete mode 100644 pkg/naming/etcd/etcd.go delete mode 100644 pkg/naming/naming.go delete mode 100644 pkg/naming/opt.go delete mode 100644 pkg/naming/opt_test.go delete mode 100644 pkg/naming/zookeeper/zookeeper.go delete mode 100644 pkg/net/criticality/criticality.go delete mode 100644 pkg/net/http/blademaster/README.md delete mode 100644 pkg/net/http/blademaster/binding/binding.go delete mode 100644 pkg/net/http/blademaster/binding/binding_test.go delete mode 100644 pkg/net/http/blademaster/binding/default_validator.go delete mode 100644 pkg/net/http/blademaster/binding/example/test.pb.go delete mode 100644 pkg/net/http/blademaster/binding/example/test.proto delete mode 100644 pkg/net/http/blademaster/binding/example_test.go delete mode 100644 pkg/net/http/blademaster/binding/form.go delete mode 100644 pkg/net/http/blademaster/binding/form_mapping.go delete mode 100644 pkg/net/http/blademaster/binding/json.go delete mode 100644 pkg/net/http/blademaster/binding/query.go delete mode 100644 pkg/net/http/blademaster/binding/tags.go delete mode 100644 pkg/net/http/blademaster/binding/validate_test.go delete mode 100644 pkg/net/http/blademaster/binding/xml.go delete mode 100644 pkg/net/http/blademaster/client.go delete mode 100644 pkg/net/http/blademaster/context.go delete mode 100644 pkg/net/http/blademaster/cors.go delete mode 100644 pkg/net/http/blademaster/criticality.go delete mode 100644 pkg/net/http/blademaster/csrf.go delete mode 100644 pkg/net/http/blademaster/logger.go delete mode 100644 pkg/net/http/blademaster/metadata.go delete mode 100644 pkg/net/http/blademaster/metrics.go delete mode 100644 pkg/net/http/blademaster/perf.go delete mode 100644 pkg/net/http/blademaster/prometheus.go delete mode 100644 pkg/net/http/blademaster/ratelimit.go delete mode 100644 pkg/net/http/blademaster/recovery.go delete mode 100644 pkg/net/http/blademaster/render/data.go delete mode 100644 pkg/net/http/blademaster/render/json.go delete mode 100644 pkg/net/http/blademaster/render/protobuf.go delete mode 100644 pkg/net/http/blademaster/render/redirect.go delete mode 100644 pkg/net/http/blademaster/render/render.go delete mode 100644 pkg/net/http/blademaster/render/render.pb.go delete mode 100644 pkg/net/http/blademaster/render/render.proto delete mode 100644 pkg/net/http/blademaster/render/string.go delete mode 100644 pkg/net/http/blademaster/render/xml.go delete mode 100644 pkg/net/http/blademaster/routergroup.go delete mode 100644 pkg/net/http/blademaster/server.go delete mode 100644 pkg/net/http/blademaster/server_test.go delete mode 100644 pkg/net/http/blademaster/trace.go delete mode 100644 pkg/net/http/blademaster/tree.go delete mode 100644 pkg/net/http/blademaster/utils.go delete mode 100644 pkg/net/ip/ip.go delete mode 100644 pkg/net/metadata/README.md delete mode 100644 pkg/net/metadata/key.go delete mode 100644 pkg/net/metadata/metadata.go delete mode 100644 pkg/net/metadata/metadata_test.go delete mode 100644 pkg/net/netutil/backoff.go delete mode 100644 pkg/net/netutil/breaker/README.md delete mode 100644 pkg/net/netutil/breaker/breaker.go delete mode 100644 pkg/net/netutil/breaker/breaker_test.go delete mode 100644 pkg/net/netutil/breaker/example_test.go delete mode 100644 pkg/net/netutil/breaker/sre_breaker.go delete mode 100644 pkg/net/netutil/breaker/sre_breaker_test.go delete mode 100644 pkg/net/rpc/warden/README.md delete mode 100644 pkg/net/rpc/warden/balancer/p2c/README.md delete mode 100644 pkg/net/rpc/warden/balancer/p2c/p2c.go delete mode 100644 pkg/net/rpc/warden/balancer/p2c/p2c_test.go delete mode 100644 pkg/net/rpc/warden/balancer/wrr/README.md delete mode 100644 pkg/net/rpc/warden/balancer/wrr/wrr.go delete mode 100644 pkg/net/rpc/warden/balancer/wrr/wrr_test.go delete mode 100644 pkg/net/rpc/warden/client.go delete mode 100644 pkg/net/rpc/warden/client_test.go delete mode 100644 pkg/net/rpc/warden/exapmle_test.go delete mode 100644 pkg/net/rpc/warden/internal/benchmark/bench/client/client.go delete mode 100644 pkg/net/rpc/warden/internal/benchmark/bench/proto/hello.pb.go delete mode 100644 pkg/net/rpc/warden/internal/benchmark/bench/proto/hello.proto delete mode 100644 pkg/net/rpc/warden/internal/benchmark/bench/server/server.go delete mode 100755 pkg/net/rpc/warden/internal/benchmark/helloworld/client.sh delete mode 100644 pkg/net/rpc/warden/internal/benchmark/helloworld/client/greeter_client.go delete mode 100644 pkg/net/rpc/warden/internal/benchmark/helloworld/server/greeter_server.go delete mode 100644 pkg/net/rpc/warden/internal/encoding/json/json.go delete mode 100644 pkg/net/rpc/warden/internal/examples/client/client.go delete mode 100644 pkg/net/rpc/warden/internal/examples/grpcDebug/client.go delete mode 100644 pkg/net/rpc/warden/internal/examples/grpcDebug/data.json delete mode 100644 pkg/net/rpc/warden/internal/examples/server/main.go delete mode 100644 pkg/net/rpc/warden/internal/metadata/metadata.go delete mode 100644 pkg/net/rpc/warden/internal/proto/testproto/hello.pb.go delete mode 100644 pkg/net/rpc/warden/internal/proto/testproto/hello.proto delete mode 100644 pkg/net/rpc/warden/internal/status/status.go delete mode 100644 pkg/net/rpc/warden/internal/status/status_test.go delete mode 100644 pkg/net/rpc/warden/logging.go delete mode 100644 pkg/net/rpc/warden/logging_test.go delete mode 100644 pkg/net/rpc/warden/metrics.go delete mode 100644 pkg/net/rpc/warden/ratelimiter/ratelimiter.go delete mode 100644 pkg/net/rpc/warden/recovery.go delete mode 100644 pkg/net/rpc/warden/resolver/README.md delete mode 100644 pkg/net/rpc/warden/resolver/direct/README.md delete mode 100644 pkg/net/rpc/warden/resolver/direct/direct.go delete mode 100644 pkg/net/rpc/warden/resolver/direct/test/direct_test.go delete mode 100644 pkg/net/rpc/warden/resolver/resolver.go delete mode 100644 pkg/net/rpc/warden/resolver/test/mockdiscovery.go delete mode 100644 pkg/net/rpc/warden/resolver/test/resovler_test.go delete mode 100644 pkg/net/rpc/warden/resolver/util.go delete mode 100644 pkg/net/rpc/warden/server.go delete mode 100644 pkg/net/rpc/warden/server_test.go delete mode 100644 pkg/net/rpc/warden/stats.go delete mode 100644 pkg/net/rpc/warden/validate.go delete mode 100644 pkg/net/trace/README.md delete mode 100644 pkg/net/trace/config.go delete mode 100644 pkg/net/trace/config_test.go delete mode 100644 pkg/net/trace/const.go delete mode 100644 pkg/net/trace/context.go delete mode 100644 pkg/net/trace/context_test.go delete mode 100644 pkg/net/trace/dapper.go delete mode 100644 pkg/net/trace/dapper_test.go delete mode 100644 pkg/net/trace/jaeger/config.go delete mode 100644 pkg/net/trace/jaeger/http_transport.go delete mode 100644 pkg/net/trace/jaeger/jaeger.go delete mode 100644 pkg/net/trace/jaeger/jaeger_test.go delete mode 100644 pkg/net/trace/jaeger/reference.go delete mode 100644 pkg/net/trace/jaeger/span.go delete mode 100644 pkg/net/trace/jaeger/span_context.go delete mode 100644 pkg/net/trace/marshal.go delete mode 100644 pkg/net/trace/marshal_test.go delete mode 100644 pkg/net/trace/mocktrace/mocktrace.go delete mode 100644 pkg/net/trace/mocktrace/mocktrace_test.go delete mode 100644 pkg/net/trace/noop.go delete mode 100644 pkg/net/trace/option.go delete mode 100644 pkg/net/trace/propagation.go delete mode 100644 pkg/net/trace/proto/span.pb.go delete mode 100644 pkg/net/trace/proto/span.proto delete mode 100644 pkg/net/trace/report.go delete mode 100644 pkg/net/trace/report_test.go delete mode 100644 pkg/net/trace/sample.go delete mode 100644 pkg/net/trace/sample_test.go delete mode 100644 pkg/net/trace/span.go delete mode 100644 pkg/net/trace/span_test.go delete mode 100644 pkg/net/trace/tag.go delete mode 100644 pkg/net/trace/tag_test.go delete mode 100644 pkg/net/trace/tracer.go delete mode 100644 pkg/net/trace/util.go delete mode 100644 pkg/net/trace/util_test.go delete mode 100644 pkg/net/trace/zipkin/config.go delete mode 100644 pkg/net/trace/zipkin/zipkin.go delete mode 100644 pkg/net/trace/zipkin/zipkin_test.go delete mode 100644 pkg/ratelimit/README.md delete mode 100644 pkg/ratelimit/bbr/bbr.go delete mode 100644 pkg/ratelimit/bbr/bbr_test.go delete mode 100644 pkg/ratelimit/limiter.go delete mode 100644 pkg/stat/README.md delete mode 100644 pkg/stat/metric/counter.go delete mode 100644 pkg/stat/metric/counter_test.go delete mode 100644 pkg/stat/metric/gauge.go delete mode 100644 pkg/stat/metric/gauge_test.go delete mode 100644 pkg/stat/metric/histogram.go delete mode 100644 pkg/stat/metric/iterator.go delete mode 100644 pkg/stat/metric/metric.go delete mode 100644 pkg/stat/metric/point_gauge.go delete mode 100644 pkg/stat/metric/point_gauge_test.go delete mode 100644 pkg/stat/metric/point_policy.go delete mode 100644 pkg/stat/metric/reduce.go delete mode 100644 pkg/stat/metric/reduce_test.go delete mode 100644 pkg/stat/metric/rolling_counter.go delete mode 100644 pkg/stat/metric/rolling_counter_test.go delete mode 100644 pkg/stat/metric/rolling_gauge.go delete mode 100644 pkg/stat/metric/rolling_gauge_test.go delete mode 100644 pkg/stat/metric/rolling_policy.go delete mode 100644 pkg/stat/metric/rolling_policy_test.go delete mode 100644 pkg/stat/metric/window.go delete mode 100644 pkg/stat/metric/window_test.go delete mode 100644 pkg/stat/sys/cpu/README.md delete mode 100644 pkg/stat/sys/cpu/cgroup.go delete mode 100644 pkg/stat/sys/cpu/cgroupCPU.go delete mode 100644 pkg/stat/sys/cpu/cgroup_test.go delete mode 100644 pkg/stat/sys/cpu/cpu.go delete mode 100644 pkg/stat/sys/cpu/cpu_test.go delete mode 100644 pkg/stat/sys/cpu/psutilCPU.go delete mode 100644 pkg/stat/sys/cpu/stat_test.go delete mode 100644 pkg/stat/sys/cpu/util.go delete mode 100644 pkg/str/str.go delete mode 100644 pkg/str/str_test.go delete mode 100644 pkg/sync/errgroup/README.md delete mode 100644 pkg/sync/errgroup/doc.go delete mode 100644 pkg/sync/errgroup/errgroup.go delete mode 100644 pkg/sync/errgroup/errgroup_test.go delete mode 100644 pkg/sync/errgroup/example_test.go delete mode 100755 pkg/sync/pipeline/CHANGELOG.md delete mode 100644 pkg/sync/pipeline/README.md delete mode 100755 pkg/sync/pipeline/fanout/CHANGELOG.md delete mode 100644 pkg/sync/pipeline/fanout/README.md delete mode 100644 pkg/sync/pipeline/fanout/example_test.go delete mode 100644 pkg/sync/pipeline/fanout/fanout.go delete mode 100644 pkg/sync/pipeline/fanout/fanout_test.go delete mode 100644 pkg/sync/pipeline/fanout/metrics.go delete mode 100644 pkg/sync/pipeline/pipeline.go delete mode 100644 pkg/sync/pipeline/pipeline_test.go delete mode 100644 pkg/testing/lich/README.md delete mode 100644 pkg/testing/lich/composer.go delete mode 100644 pkg/testing/lich/healthcheck.go delete mode 100644 pkg/testing/lich/model.go delete mode 100644 pkg/time/README.md delete mode 100644 pkg/time/time.go delete mode 100644 pkg/time/time_test.go create mode 100644 registry/registry.go delete mode 100644 third_party/CHANGELOG.md create mode 100644 third_party/README.md delete mode 100644 third_party/github.com/gogo/protobuf/AUTHORS delete mode 100644 third_party/github.com/gogo/protobuf/CONTRIBUTORS delete mode 100644 third_party/github.com/gogo/protobuf/LICENSE delete mode 100644 third_party/github.com/gogo/protobuf/gogoproto/gogo.proto delete mode 100644 third_party/google/api/README.md delete mode 100644 third_party/google/api/auth.proto delete mode 100644 third_party/google/api/backend.proto delete mode 100644 third_party/google/api/billing.proto delete mode 100644 third_party/google/api/client.proto delete mode 100644 third_party/google/api/config_change.proto delete mode 100644 third_party/google/api/consumer.proto delete mode 100644 third_party/google/api/context.proto delete mode 100644 third_party/google/api/control.proto delete mode 100644 third_party/google/api/distribution.proto delete mode 100644 third_party/google/api/documentation.proto delete mode 100644 third_party/google/api/endpoint.proto delete mode 100644 third_party/google/api/experimental/authorization_config.proto delete mode 100644 third_party/google/api/experimental/experimental.proto delete mode 100644 third_party/google/api/expr/artman_cel.yaml delete mode 100644 third_party/google/api/expr/cel.yaml delete mode 100644 third_party/google/api/expr/v1alpha1/cel_gapic.yaml delete mode 100644 third_party/google/api/expr/v1alpha1/cel_service.proto delete mode 100644 third_party/google/api/expr/v1alpha1/checked.proto delete mode 100644 third_party/google/api/expr/v1alpha1/conformance_service.proto delete mode 100644 third_party/google/api/expr/v1alpha1/eval.proto delete mode 100644 third_party/google/api/expr/v1alpha1/explain.proto delete mode 100644 third_party/google/api/expr/v1alpha1/syntax.proto delete mode 100644 third_party/google/api/expr/v1alpha1/value.proto delete mode 100644 third_party/google/api/expr/v1beta1/decl.proto delete mode 100644 third_party/google/api/expr/v1beta1/eval.proto delete mode 100644 third_party/google/api/expr/v1beta1/expr.proto delete mode 100644 third_party/google/api/expr/v1beta1/source.proto delete mode 100644 third_party/google/api/expr/v1beta1/value.proto delete mode 100644 third_party/google/api/field_behavior.proto delete mode 100644 third_party/google/api/label.proto delete mode 100644 third_party/google/api/launch_stage.proto delete mode 100644 third_party/google/api/log.proto delete mode 100644 third_party/google/api/logging.proto delete mode 100644 third_party/google/api/metric.proto delete mode 100644 third_party/google/api/monitored_resource.proto delete mode 100644 third_party/google/api/monitoring.proto delete mode 100644 third_party/google/api/quota.proto delete mode 100644 third_party/google/api/resource.proto delete mode 100644 third_party/google/api/service.proto delete mode 100644 third_party/google/api/serviceconfig.yaml delete mode 100644 third_party/google/api/servicecontrol/README.md delete mode 100644 third_party/google/api/servicecontrol/v1/check_error.proto delete mode 100644 third_party/google/api/servicecontrol/v1/distribution.proto delete mode 100644 third_party/google/api/servicecontrol/v1/log_entry.proto delete mode 100644 third_party/google/api/servicecontrol/v1/metric_value.proto delete mode 100644 third_party/google/api/servicecontrol/v1/operation.proto delete mode 100644 third_party/google/api/servicecontrol/v1/quota_controller.proto delete mode 100644 third_party/google/api/servicecontrol/v1/service_controller.proto delete mode 100644 third_party/google/api/servicemanagement/README.md delete mode 100644 third_party/google/api/servicemanagement/artman_servicemanagement_v1.yaml delete mode 100644 third_party/google/api/servicemanagement/servicemanagement_v1.yaml delete mode 100644 third_party/google/api/servicemanagement/v1/resources.proto delete mode 100644 third_party/google/api/servicemanagement/v1/servicemanagement_gapic.yaml delete mode 100644 third_party/google/api/servicemanagement/v1/servicemanager.proto delete mode 100644 third_party/google/api/source_info.proto delete mode 100644 third_party/google/api/system_parameter.proto delete mode 100644 third_party/google/api/usage.proto delete mode 100644 tool/kratos-gen-bts/README.md delete mode 100644 tool/kratos-gen-bts/header_template.go delete mode 100644 tool/kratos-gen-bts/main.go delete mode 100644 tool/kratos-gen-bts/multi_template.go delete mode 100644 tool/kratos-gen-bts/none_template.go delete mode 100644 tool/kratos-gen-bts/single_template.go delete mode 100644 tool/kratos-gen-bts/testdata/dao.bts.go delete mode 100644 tool/kratos-gen-bts/testdata/dao.go delete mode 100644 tool/kratos-gen-bts/testdata/multi.go delete mode 100644 tool/kratos-gen-bts/testdata/multi_test.go delete mode 100644 tool/kratos-gen-bts/testdata/none.go delete mode 100644 tool/kratos-gen-bts/testdata/none_test.go delete mode 100644 tool/kratos-gen-bts/testdata/single.go delete mode 100644 tool/kratos-gen-bts/testdata/single_test.go delete mode 100644 tool/kratos-gen-mc/README.md delete mode 100644 tool/kratos-gen-mc/header_template.go delete mode 100644 tool/kratos-gen-mc/main.go delete mode 100644 tool/kratos-gen-mc/multi_template.go delete mode 100644 tool/kratos-gen-mc/none_template.go delete mode 100644 tool/kratos-gen-mc/single_template.go delete mode 100644 tool/kratos-gen-mc/testdata/dao.go delete mode 100644 tool/kratos-gen-mc/testdata/dao_test.go delete mode 100644 tool/kratos-gen-mc/testdata/mc.cache.go delete mode 100644 tool/kratos-gen-mc/testdata/model.pb.go delete mode 100644 tool/kratos-gen-mc/testdata/model.proto delete mode 100644 tool/kratos-gen-project/main-packr.go delete mode 100644 tool/kratos-gen-project/main.go delete mode 100644 tool/kratos-gen-project/new.go delete mode 100644 tool/kratos-gen-project/packrd/packed-packr.go delete mode 100644 tool/kratos-gen-project/project.go delete mode 100644 tool/kratos-gen-project/templates/all/CHANGELOG.md delete mode 100644 tool/kratos-gen-project/templates/all/OWNERS delete mode 100644 tool/kratos-gen-project/templates/all/README.md delete mode 100644 tool/kratos-gen-project/templates/all/api/api.proto delete mode 100644 tool/kratos-gen-project/templates/all/api/client.go.tmpl delete mode 100644 tool/kratos-gen-project/templates/all/cmd/main.go.tmpl delete mode 100644 tool/kratos-gen-project/templates/all/configs/application.toml delete mode 100644 tool/kratos-gen-project/templates/all/configs/db.toml delete mode 100644 tool/kratos-gen-project/templates/all/configs/grpc.toml delete mode 100644 tool/kratos-gen-project/templates/all/configs/http.toml delete mode 100644 tool/kratos-gen-project/templates/all/configs/memcache.toml delete mode 100644 tool/kratos-gen-project/templates/all/configs/redis.toml delete mode 100644 tool/kratos-gen-project/templates/all/go.mod.tmpl delete mode 100644 tool/kratos-gen-project/templates/all/internal/dao/dao.go.tmpl delete mode 100644 tool/kratos-gen-project/templates/all/internal/dao/dao_test.go.tmpl delete mode 100644 tool/kratos-gen-project/templates/all/internal/dao/db.go.tmpl delete mode 100644 tool/kratos-gen-project/templates/all/internal/dao/mc.go.tmpl delete mode 100644 tool/kratos-gen-project/templates/all/internal/dao/redis.go.tmpl delete mode 100644 tool/kratos-gen-project/templates/all/internal/dao/wire.go.tmpl delete mode 100644 tool/kratos-gen-project/templates/all/internal/di/app.go.tmpl delete mode 100644 tool/kratos-gen-project/templates/all/internal/di/wire.go.tmpl delete mode 100644 tool/kratos-gen-project/templates/all/internal/model/model.go.tmpl delete mode 100644 tool/kratos-gen-project/templates/all/internal/server/grpc/server.go.tmpl delete mode 100644 tool/kratos-gen-project/templates/all/internal/server/http/server.go.tmpl delete mode 100644 tool/kratos-gen-project/templates/all/internal/service/service.go.tmpl delete mode 100644 tool/kratos-gen-project/templates/all/test/0_db.sql delete mode 100644 tool/kratos-gen-project/templates/all/test/1_data.sql delete mode 100644 tool/kratos-gen-project/templates/all/test/application.toml delete mode 100644 tool/kratos-gen-project/templates/all/test/db.toml delete mode 100644 tool/kratos-gen-project/templates/all/test/docker-compose.yaml delete mode 100644 tool/kratos-gen-project/templates/all/test/grpc.toml delete mode 100644 tool/kratos-gen-project/templates/all/test/http.toml delete mode 100644 tool/kratos-gen-project/templates/all/test/memcache.toml delete mode 100644 tool/kratos-gen-project/templates/all/test/redis.toml delete mode 100644 tool/kratos-gen-project/templates/grpc/CHANGELOG.md delete mode 100644 tool/kratos-gen-project/templates/grpc/OWNERS delete mode 100644 tool/kratos-gen-project/templates/grpc/README.md delete mode 100644 tool/kratos-gen-project/templates/grpc/api/api.proto delete mode 100644 tool/kratos-gen-project/templates/grpc/api/client.go.tmpl delete mode 100644 tool/kratos-gen-project/templates/grpc/cmd/main.go.tmpl delete mode 100644 tool/kratos-gen-project/templates/grpc/configs/application.toml delete mode 100644 tool/kratos-gen-project/templates/grpc/configs/db.toml delete mode 100644 tool/kratos-gen-project/templates/grpc/configs/grpc.toml delete mode 100644 tool/kratos-gen-project/templates/grpc/configs/memcache.toml delete mode 100644 tool/kratos-gen-project/templates/grpc/configs/redis.toml delete mode 100644 tool/kratos-gen-project/templates/grpc/go.mod.tmpl delete mode 100644 tool/kratos-gen-project/templates/grpc/internal/dao/dao.go.tmpl delete mode 100644 tool/kratos-gen-project/templates/grpc/internal/dao/dao_test.go.tmpl delete mode 100644 tool/kratos-gen-project/templates/grpc/internal/dao/db.go.tmpl delete mode 100644 tool/kratos-gen-project/templates/grpc/internal/dao/mc.go.tmpl delete mode 100644 tool/kratos-gen-project/templates/grpc/internal/dao/redis.go.tmpl delete mode 100644 tool/kratos-gen-project/templates/grpc/internal/dao/wire.go.tmpl delete mode 100644 tool/kratos-gen-project/templates/grpc/internal/di/app.go.tmpl delete mode 100644 tool/kratos-gen-project/templates/grpc/internal/di/wire.go.tmpl delete mode 100644 tool/kratos-gen-project/templates/grpc/internal/model/model.go.tmpl delete mode 100644 tool/kratos-gen-project/templates/grpc/internal/server/grpc/server.go.tmpl delete mode 100644 tool/kratos-gen-project/templates/grpc/internal/service/service.go.tmpl delete mode 100644 tool/kratos-gen-project/templates/grpc/test/0_db.sql delete mode 100644 tool/kratos-gen-project/templates/grpc/test/1_data.sql delete mode 100644 tool/kratos-gen-project/templates/grpc/test/application.toml delete mode 100644 tool/kratos-gen-project/templates/grpc/test/db.toml delete mode 100644 tool/kratos-gen-project/templates/grpc/test/docker-compose.yaml delete mode 100644 tool/kratos-gen-project/templates/grpc/test/grpc.toml delete mode 100644 tool/kratos-gen-project/templates/grpc/test/memcache.toml delete mode 100644 tool/kratos-gen-project/templates/grpc/test/redis.toml delete mode 100644 tool/kratos-gen-project/templates/http/CHANGELOG.md delete mode 100644 tool/kratos-gen-project/templates/http/OWNERS delete mode 100644 tool/kratos-gen-project/templates/http/README.md delete mode 100644 tool/kratos-gen-project/templates/http/api/api.proto delete mode 100644 tool/kratos-gen-project/templates/http/api/client.go.tmpl delete mode 100644 tool/kratos-gen-project/templates/http/cmd/main.go.tmpl delete mode 100644 tool/kratos-gen-project/templates/http/configs/application.toml delete mode 100644 tool/kratos-gen-project/templates/http/configs/db.toml delete mode 100644 tool/kratos-gen-project/templates/http/configs/http.toml delete mode 100644 tool/kratos-gen-project/templates/http/configs/memcache.toml delete mode 100644 tool/kratos-gen-project/templates/http/configs/redis.toml delete mode 100644 tool/kratos-gen-project/templates/http/go.mod.tmpl delete mode 100644 tool/kratos-gen-project/templates/http/internal/dao/dao.go.tmpl delete mode 100644 tool/kratos-gen-project/templates/http/internal/dao/dao_test.go.tmpl delete mode 100644 tool/kratos-gen-project/templates/http/internal/dao/db.go.tmpl delete mode 100644 tool/kratos-gen-project/templates/http/internal/dao/mc.go.tmpl delete mode 100644 tool/kratos-gen-project/templates/http/internal/dao/redis.go.tmpl delete mode 100644 tool/kratos-gen-project/templates/http/internal/dao/wire.go.tmpl delete mode 100644 tool/kratos-gen-project/templates/http/internal/di/app.go.tmpl delete mode 100644 tool/kratos-gen-project/templates/http/internal/di/wire.go.tmpl delete mode 100644 tool/kratos-gen-project/templates/http/internal/model/model.go.tmpl delete mode 100644 tool/kratos-gen-project/templates/http/internal/server/http/server.go.tmpl delete mode 100644 tool/kratos-gen-project/templates/http/internal/service/service.go.tmpl delete mode 100644 tool/kratos-gen-project/templates/http/test/0_db.sql delete mode 100644 tool/kratos-gen-project/templates/http/test/1_data.sql delete mode 100644 tool/kratos-gen-project/templates/http/test/application.toml delete mode 100644 tool/kratos-gen-project/templates/http/test/db.toml delete mode 100644 tool/kratos-gen-project/templates/http/test/docker-compose.yaml delete mode 100644 tool/kratos-gen-project/templates/http/test/http.toml delete mode 100644 tool/kratos-gen-project/templates/http/test/memcache.toml delete mode 100644 tool/kratos-gen-project/templates/http/test/redis.toml delete mode 100755 tool/kratos-gen-project/testdata/test_in_gomod.sh delete mode 100755 tool/kratos-gen-project/testdata/test_not_in_gomod.sh delete mode 100644 tool/kratos-protoc/bm.go delete mode 100644 tool/kratos-protoc/ecode.go delete mode 100644 tool/kratos-protoc/grpc.go delete mode 100644 tool/kratos-protoc/main.go delete mode 100644 tool/kratos-protoc/protoc.go delete mode 100644 tool/kratos-protoc/swagger.go delete mode 100644 tool/kratos/README.MD delete mode 100644 tool/kratos/build.go delete mode 100644 tool/kratos/env.go delete mode 100644 tool/kratos/main.go delete mode 100644 tool/kratos/run.go delete mode 100644 tool/kratos/tool.go delete mode 100644 tool/kratos/tool_index.go delete mode 100644 tool/kratos/version.go delete mode 100644 tool/pkg/common.go delete mode 100644 tool/protobuf/pkg/extensions/gogoproto/gogo.pb.go delete mode 100644 tool/protobuf/pkg/extensions/gogoproto/gogo.pb.golden delete mode 100644 tool/protobuf/pkg/extensions/gogoproto/gogo.proto delete mode 100644 tool/protobuf/pkg/extensions/google/api/annotations.proto delete mode 100644 tool/protobuf/pkg/extensions/google/api/http.proto delete mode 100644 tool/protobuf/pkg/gen/main.go delete mode 100644 tool/protobuf/pkg/generator/command_line.go delete mode 100644 tool/protobuf/pkg/generator/generator.go delete mode 100644 tool/protobuf/pkg/generator/helper.go delete mode 100644 tool/protobuf/pkg/generator/http.go delete mode 100644 tool/protobuf/pkg/naming/go_naming.go delete mode 100644 tool/protobuf/pkg/naming/naming.go delete mode 100644 tool/protobuf/pkg/project/project.go delete mode 100644 tool/protobuf/pkg/tag/ext_tags.go delete mode 100644 tool/protobuf/pkg/tag/tags.go delete mode 100644 tool/protobuf/pkg/typemap/typemap.go delete mode 100644 tool/protobuf/pkg/utils/stringutils.go delete mode 100644 tool/protobuf/pkg/utils/utils.go delete mode 100644 tool/protobuf/protoc-gen-bm/generator/generator.go delete mode 100644 tool/protobuf/protoc-gen-bm/generator/generator_test.go delete mode 100644 tool/protobuf/protoc-gen-bm/main.go delete mode 100644 tool/protobuf/protoc-gen-bswagger/generator.go delete mode 100644 tool/protobuf/protoc-gen-bswagger/main.go delete mode 100644 tool/protobuf/protoc-gen-bswagger/types.go delete mode 100644 tool/protobuf/protoc-gen-ecode/generator/generator.go delete mode 100644 tool/protobuf/protoc-gen-ecode/generator/generator_test.go delete mode 100644 tool/protobuf/protoc-gen-ecode/main.go delete mode 100644 tool/testcli/README.md delete mode 100644 tool/testcli/docker-compose.yaml delete mode 100644 tool/testcli/main.go delete mode 100644 tool/testgen/README.md delete mode 100644 tool/testgen/gen.go delete mode 100644 tool/testgen/main.go delete mode 100644 tool/testgen/parser.go delete mode 100644 tool/testgen/templete.go delete mode 100644 tool/testgen/utils.go create mode 100644 transport/grpc/client.go create mode 100644 transport/grpc/context.go create mode 100644 transport/grpc/resolver/direct/builder.go create mode 100644 transport/grpc/resolver/direct/resolver.go create mode 100644 transport/grpc/resolver/discovery/builder.go create mode 100644 transport/grpc/resolver/discovery/resolver.go create mode 100644 transport/grpc/server.go create mode 100644 transport/grpc/server_test.go create mode 100644 transport/http/bind.go create mode 100644 transport/http/client.go create mode 100644 transport/http/context.go create mode 100644 transport/http/default.go create mode 100644 transport/http/errors.go create mode 100644 transport/http/route.go create mode 100644 transport/http/server.go create mode 100644 transport/http/server_test.go create mode 100644 transport/http/service.go create mode 100644 transport/http/service_test.go create mode 100644 transport/transport.go diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml new file mode 100644 index 000000000..ffbed81e6 --- /dev/null +++ b/.github/FUNDING.yml @@ -0,0 +1,3 @@ +# These are supported funding model platforms + +github: [tonybase] diff --git a/.github/workflows/go.yml b/.github/workflows/go.yml deleted file mode 100644 index 704643d43..000000000 --- a/.github/workflows/go.yml +++ /dev/null @@ -1,131 +0,0 @@ -name: Go - -on: - push: - branches: [ master ] - pull_request: - branches: [ master ] - -jobs: - - build: - name: Build on ${{ matrix.os }} - Go${{ matrix.go_version }} - runs-on: ${{ matrix.os }} - strategy: - matrix: - go_version: - - 1.13 - os: - - ubuntu-latest - - steps: - - - name: Set up Go ${{ matrix.go_version }} - uses: actions/setup-go@v1 - with: - go-version: ${{ matrix.go_version }} - id: go - - - name: Set up Env - run: | - echo "GOPATH=$(go env GOPATH)" >> $GITHUB_ENV - echo "$(go env GOPATH)/bin" >> $GITHUB_PATH - - - name: Check out code into the Go module directory - uses: actions/checkout@v2 - - - name: Cache dependencies - uses: actions/cache@v2 - with: - # Cache - path: ~/go/pkg/mod - # Cache key - key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }} - # An ordered list of keys to use for restoring the cache if no cache hit occurred for key - restore-keys: | - ${{ runner.os }}-go- - - - name: Get dependencies - run: | - go get -v -t -d ./... - if [ -f Gopkg.toml ]; then - curl https://raw.githubusercontent.com/golang/dep/master/install.sh | sh - dep ensure - fi - - - name: Build - run: go build ./... - - - name: Golangci - run: | - curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -b $(go env GOPATH)/bin v1.33.0 - golangci-lint run --out-format=github-actions - - - name: Test - run: go test ./... -coverprofile=coverage.txt -covermode=atomic - - - name: Coverage - run: bash <(curl -s https://codecov.io/bash) - - scaffold: - - name: Scaffold Test on ${{ matrix.os }} - Go${{ matrix.go_version }} - runs-on: ${{ matrix.os }} - strategy: - matrix: - go_version: - - 1.13 - os: - - ubuntu-latest - - steps: - - - name: Set up Go ${{ matrix.go_version }} - uses: actions/setup-go@v1 - with: - go-version: ${{ matrix.go_version }} - id: go - - - name: Set up Env - run: | - echo "GOPATH=$(go env GOPATH)" >> $GITHUB_ENV - echo "$(go env GOPATH)/bin" >> $GITHUB_PATH - - - name: Check out code into the Go module directory - uses: actions/checkout@v2 - - - name: Cache dependencies - uses: actions/cache@v2 - with: - # Cache - path: ~/go/pkg/mod - # Cache key - key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }} - # An ordered list of keys to use for restoring the cache if no cache hit occurred for key - restore-keys: | - ${{ runner.os }}-go- - - - name: Get dependencies - run: | - go get -v -t -d ./... - if [ -f Gopkg.toml ]; then - curl https://raw.githubusercontent.com/golang/dep/master/install.sh | sh - dep ensure - fi - wget https://github.com/google/protobuf/releases/download/v3.11.4/protoc-3.11.4-linux-x86_64.zip - unzip protoc-3.11.4-linux-x86_64.zip - chmod +x bin/protoc - sudo mv bin/protoc /usr/local/bin - sudo mv include /usr/local/bin - go get -u github.com/golang/protobuf/protoc-gen-go - go get -u github.com/gogo/protobuf/protoc-gen-gofast - - - name: Tool - run: | - go install ./... - mkdir -p $GOPATH/src - cp -R ../kratos $GOPATH/src - cd $GOPATH/src - kratos new kratos-demo - cd kratos-demo - go build ./... diff --git a/.gitignore b/.gitignore index f9f02aeb8..c29907463 100644 --- a/.gitignore +++ b/.gitignore @@ -1,32 +1,36 @@ -# idea ignore -.idea/ -*.ipr -*.iml -*.iws -.vscode/ - -# temp ignore -*.log -*.cache -*.diff +# Reference https://github.com/github/gitignore/blob/master/Go.gitignore +# Binaries for programs and plugins *.exe *.exe~ -*.patch -*.swp -*.tmp +*.dll +*.so +*.dylib -# system ignore -.DS_Store +# Test binary, built with `go test -c` +*.test + +# Output of the go coverage tool, specifically when used with LiteIDE +*.out + +# Dependency directories (remove the comment below to include it) +vendor/ + +# Compiled Object files, Static and Dynamic libs (Shared Objects) +*.o +*.a +*.so + +# OS General Thumbs.db +.DS_Store # project *.cert *.key -tool/kratos/kratos -tool/kratos-protoc/kratos-protoc -tool/kratos-gen-bts/kratos-gen-bts -tool/kratos-gen-mc/kratos-gen-mc -tool/kratos/kratos-protoc/kratos-protoc -tool/kratos/protobuf/protoc-gen-bm/protoc-gen-bm -tool/kratos/protobuf/protoc-gen-ecode/protoc-gen-ecode -tool/kratos/protobuf/protoc-gen-bswagger/protoc-gen-bswagger +*.log +bin/ + +# Develop tools +.vscode/ +.idea/ +*.swp diff --git a/.golangci.yml b/.golangci.yml deleted file mode 100644 index 3ce2dbc0a..000000000 --- a/.golangci.yml +++ /dev/null @@ -1,138 +0,0 @@ -# [index] https://github.com/golangci/golangci-lint -# [example] https://github.com/golangci/golangci-lint/blob/master/.golangci.example.yml - -run: - tests: true #是否包含测试文件 - issues-exit-code: 0 - -linters-settings: - # govet: - # check-shadowing: true #启用了对同名变量名在函数中被隐藏的警告 - gofmt: - simplify: true - goimports: - local-prefixes: "github.com/go-kratos/kratos" # 格式化代码时,本地代码单独块 - gocritic: - enabled-tags: - - diagnostic - # - style - # - performance - disabled-checks: - #- wrapperFunc - #- dupImport # https://github.com/go-critic/go-critic/issues/845 - - commentedOutCode - - ifElseChain - - elseif - settings: # settings passed to gocritic - captLocal: # must be valid enabled check name - paramsOnly: true - # rangeValCopy: - # sizeThreshold: 32 - lll: - line-length: 500 - funlen: - lines: 500 - statements: 500 - gocyclo: - min-complexity: 100 - -linters: - disable-all: true - enable: - # https://golangci-lint.run/usage/configuration/ - - bodyclose # http.resp.body 内存泄露检查 - - deadcode # 无用的变量声明检查 - - depguard # 自定义依赖包白、黑名单 控制导包 - - dogsled # 空白标识符的赋值检查 默认为2 - #- dupl # 重复代码检查 - - errcheck # 未判断的error返回值检查 - - funlen # 接口最大行数检查 - #- gochecknoinits # 包中定义init()函数检查 - #- goconst # 常量字符串检查 - - gocritic # - - gocyclo # 代码复杂度检查 - - gofmt # 优化代码 - - goimports # 自动增加和删除包 - - golint # 代码风格检查 - #- gomnd # 参数、赋值、用例、条件、操作和返回语句检查 - - goprintffuncname # - - gosec # 源代码安全检查 - - gosimple # 可以优化的代码检查 注:该工具已整合到staticcheck中 - - govet # 代码正确性检查 - - ineffassign # 无效赋值检查 - - interfacer # 建议接口的使用方式 - - lll # 行最大字符 - - misspell # 拼写错误检查 - - nakedret # 大于指定函数长度的函数的无约束返回值检查 - - nolintlint # - - rowserrcheck # sql.Rows.Err检查 - - scopelint # 循环变量引用检查,排除test文件 - - staticcheck # 静态检查 - - structcheck # 结构体字段的约束条件检查 - - stylecheck # 代码风格检查 - - typecheck # 类型检查 - - unconvert # 类型转换检查 - - unparam # 未使用参数检查 - #- unused # 未使用变量、函数检查 - - varcheck # 报告exported变量和常量 - - whitespace # 空行检查 - -severity: - # Default value is empty string. - # Set the default severity for issues. If severity rules are defined and the issues - # do not match or no severity is provided to the rule this will be the default - # severity applied. Severities should match the supported severity names of the - # selected out format. - # - Code climate: https://docs.codeclimate.com/docs/issues#issue-severity - # - Checkstyle: https://checkstyle.sourceforge.io/property_types.html#severity - # - Github: https://help.github.com/en/actions/reference/workflow-commands-for-github-actions#setting-an-error-message - default-severity: error - # The default value is false. - # If set to true severity-rules regular expressions become case sensitive. - case-sensitive: false - # Default value is empty list. - # When a list of severity rules are provided, severity information will be added to lint - # issues. Severity rules have the same filtering capability as exclude rules except you - # are allowed to specify one matcher per severity rule. - # Only affects out formats that support setting severity information. - rules: - - linters: - - dupl - - nakedret - - lll - - misspell - - goprintffuncname - - stylecheck - - deadcode - - whitespace - - unparam - - golint - - gosec - - staticcheck - - structcheck - - gocritic - - errcheck - - rowserrcheck - - unconvert - - gosimple - - rowserrcheck - - ineffassign - severity: warning - -issues: - # Excluding configuration per-path, per-linter, per-text and per-source - exclude-rules: - - path: _test\.go - linters: - - gomnd - - gocyclo - - errcheck - - dupl - - gosec - - scopelint - - interfacer - - govet - # https://github.com/go-critic/go-critic/issues/926 - - linters: - - gocritic - text: "unnecessaryDefer:" \ No newline at end of file diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 3d041a9b6..000000000 --- a/.travis.yml +++ /dev/null @@ -1,59 +0,0 @@ -language: go - -go: - - 1.12.x - - 1.13.x - -services: - - docker - -# Only clone the most recent commit. -git: - depth: 1 - -# Force-enable Go modules. This will be unnecessary when Go 1.12 lands. -env: - global: - - GO111MODULE=on - - REGION=sh - - ZONE=sh001 - - DEPLOY_ENV=dev - - DISCOVERY_NODES=127.0.0.1:7171 - - HTTP_PERF=tcp://0.0.0.0:0 - - DOCKER_COMPOSE_VERSION=1.24.1 - - ZK_VERSION=3.5.6 - -before_install: - # docker-compose - - sudo rm /usr/local/bin/docker-compose - - curl -L https://github.com/docker/compose/releases/download/${DOCKER_COMPOSE_VERSION}/docker-compose-`uname -s`-`uname -m` > docker-compose - - chmod +x docker-compose - - sudo mv docker-compose /usr/local/bin - # zookeeper - - wget "http://apache.cs.utah.edu/zookeeper/zookeeper-${ZK_VERSION}/apache-zookeeper-${ZK_VERSION}-bin.tar.gz" - - tar -xvf "apache-zookeeper-${ZK_VERSION}-bin.tar.gz" - - mv apache-zookeeper-${ZK_VERSION}-bin zk - - chmod +x ./zk/bin/zkServer.sh - -# Skip the install step. Don't `go get` dependencies. Only build with the code -# in vendor/ -install: true - -# Anything in before_script that returns a nonzero exit code will flunk the -# build and immediately stop. It's sorta like having set -e enabled in bash. -# Make sure golangci-lint is vendored. -before_script: - - curl -sfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh| sh -s -- -b $GOPATH/bin - # discovery - - curl -sfL https://raw.githubusercontent.com/bilibili/discovery/master/install.sh | sh -s -- -b $GOPATH/bin - - curl -sfL https://raw.githubusercontent.com/bilibili/discovery/master/cmd/discovery/discovery-example.toml -o $GOPATH/bin/discovery.toml - - nohup bash -c "$GOPATH/bin/discovery -conf $GOPATH/bin/discovery.toml &" - # zookeeper - - sudo ./zk/bin/zkServer.sh start ./zk/conf/zoo_sample.cfg 1> /dev/null - -script: - - go build ./... - - go test ./... - -after_success: - - golangci-lint run # run a bunch of code checkers/linters in parallel diff --git a/LICENSE b/LICENSE index 3ee19b4a1..684318c72 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ MIT License -Copyright (c) 2018 bilibili +Copyright (c) 2020 go-kratos Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/README.md b/README.md index 268c924a9..55270151c 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -![kratos](docs/img/kratos3.png) +![kratos](docs/images/kratos.png) [![Language](https://img.shields.io/badge/Language-Go-blue.svg)](https://golang.org/) [![Build Status](https://github.com/go-kratos/kratos/workflows/Go/badge.svg)](https://github.com/go-kratos/kratos/actions) @@ -8,69 +8,86 @@ # Kratos -Kratos是[bilibili](https://www.bilibili.com)开源的一套Go微服务框架,包含大量微服务相关框架及工具。 +Kratos 一套轻量级 Go 微服务框架,包含大量微服务相关框架及工具。 > 名字来源于:《战神》游戏以希腊神话为背景,讲述由凡人成为战神的奎托斯(Kratos)成为战神并展开弑神屠杀的冒险历程。 ## Goals -我们致力于提供完整的微服务研发体验,整合相关框架及工具后,微服务治理相关部分可对整体业务开发周期无感,从而更加聚焦于业务交付。对每位开发者而言,整套Kratos框架也是不错的学习仓库,可以了解和参考到[bilibili](https://www.bilibili.com)在微服务方面的技术积累和经验。 +我们致力于提供完整的微服务研发体验,整合相关框架及工具后,微服务治理相关部分可对整体业务开发周期无感,从而更加聚焦于业务交付。对每位开发者而言,整套Kratos框架也是不错的学习仓库,可以了解和参考到微服务方面的技术积累和经验。 + +### Principles + +* 简单:不过度设计,代码平实简单; +* 通用:通用业务开发所需要的基础库的功能; +* 高效:提高业务迭代的效率; +* 稳定:基础库可测试性高,覆盖率高,有线上实践安全可靠; +* 健壮:通过良好的基础库设计,减少错用; +* 高性能:性能高,但不特定为了性能做hack优化,引入unsafe; +* 扩展性:良好的接口设计,来扩展实现,或者通过新增基础库目录来扩展功能; +* 容错性:为失败设计,大量引入对SRE的理解,鲁棒性高; +* 工具链:包含大量工具链,比如cache代码生成,lint工具等等; ## Features -* HTTP Blademaster:核心基于[gin](https://github.com/gin-gonic/gin)进行模块化设计,简单易用、核心足够轻量; -* GRPC Warden:基于官方gRPC开发,集成[discovery](https://github.com/bilibili/discovery)服务发现,并融合P2C负载均衡; -* Cache:优雅的接口化设计,非常方便的缓存序列化,推荐结合代理模式[overlord](https://github.com/bilibili/overlord); -* Database:集成MySQL/HBase/TiDB,添加熔断保护和统计支持,可快速发现数据层压力; -* Config:方便易用的[paladin sdk](https://go-kratos.github.io/kratos/#/config),可配合远程配置中心,实现配置版本管理和更新; -* Log:类似[zap](https://github.com/uber-go/zap)的field实现高性能日志库,并结合log-agent实现远程日志管理; -* Trace:基于opentracing,集成了全链路trace支持(gRPC/HTTP/MySQL/Redis/Memcached); -* Kratos Tool:工具链,可快速生成标准项目,或者通过Protobuf生成代码,非常便捷使用gRPC、HTTP、swagger文档; +* APIs:协议通信以 HTTP/gRPC 为基础,通过 Protobuf 进行定义; +* Errors:通过 Protobuf 的 Enum 作为错误码定义,以及工具生成判定接口; +* Metadata:在协议通信 HTTP/gRPC 中,通过 Middleware 规范化服务元信息传递; +* Config:通过KeyValue方式实现,对多种配置源进行铺平,以Atomic方式支持动态配置; +* Logger:标准日志接口,可方便集成三方 log 库,并可通过 fluentd 收集日志; +* Metrics:统一指标接口,可以实现各种指标系统,默认集成 Prometheus; +* Tracing:遵循 OpenTracing 规范定义,以实现微服务链路追踪; +* Encoding:支持Accept和Content-Type进行自动选择内容编码; +* Transport:通用的 HTTP/gRPC 传输层,实现统一的 Middleware 插件支持; +* Server:进行基础的 Server 层封装,统一以 Options 方式配置使用; -## Quick start +## Getting Started +### Required +- [go](https://golang.org/dl/) +- [protoc](https://github.com/protocolbuffers/protobuf) +- [protoc-gen-go](https://github.com/protocolbuffers/protobuf-go) -### Requirments +### Install Kratos +``` +# 安装生成工具 +go get github.com/go-kratos/kratos/cmd/kratos +go get github.com/go-kratos/kratos/cmd/protoc-gen-go-http +go get github.com/go-kratos/kratos/cmd/protoc-gen-go-errors -Go version>=1.13 +# 或者通过 Source 安装 +cd cmd/kratos && go install +cd cmd/protoc-gen-go-http && go install +cd cmd/protoc-gen-go-errors && go install +``` +### Create a service +``` +# 创建项目模板 +kratos new helloworld -### Installation -```shell -# Linux/macOS -GO111MODULE=on && go get -u github.com/go-kratos/kratos/tool/kratos +cd helloworld +# 生成proto模板 +kratos proto add api/helloworld/helloworld.proto +# 生成service模板 +kratos proto service api/helloworld/helloworld.proto -t internal/service -# Windows (Powershell) -go env -w GO111MODULE=on ; go get -u github.com/go-kratos/kratos/tool/kratos - -# Windows (CMD) -go env -w GO111MODULE=on && go get -u github.com/go-kratos/kratos/tool/kratos - -cd $GOPATH/src -kratos new kratos-demo +# 生成api下所有proto文件 +make proto +# 编码cmd下所有main文件 +make build +# 进行单元测试 +make test ``` -通过 `kratos new` 会快速生成基于kratos库的脚手架代码,如生成 [kratos-demo](https://github.com/bilibili/kratos-demo) +## Service Layout +* [Service Layout](https://github.com/go-kratos/kratos-layout) -### Build & Run - -```shell -cd kratos-demo/cmd -go build -./cmd -conf ../configs -``` - -打开浏览器访问:[http://localhost:8000/kratos-demo/start](http://localhost:8000/kratos-demo/start),你会看到输出了`Golang 大法好 !!!` - -[快速开始](https://go-kratos.github.io/kratos/#/quickstart) [kratos工具](https://go-kratos.github.io/kratos/#/kratos-tool) - -## Documentation - -> [简体中文](https://go-kratos.github.io/kratos) -> [简体中文(国内镜像)](https://go-kratos.gitee.io/kratos/) -> [FAQ](https://go-kratos.github.io/kratos/#/FAQ) - -## 社区 -* [官方微信群](https://github.com/go-kratos/kratos/issues/682) (推荐) +## Community +* [Wechat Group](https://github.com/go-kratos/kratos/issues/682) * [Discord Group](https://discord.gg/BWzJsUJ) +* QQ Group: 716486124 + +## Sponsors and Backers + +![kratos](docs/images/alipay.png) ## License -Kratos is under the MIT license. See the [LICENSE](./LICENSE) file for details. - +Kratos is MIT licensed. See the [LICENSE](./LICENSE) file for details. diff --git a/api/README.md b/api/README.md new file mode 100644 index 000000000..159e965c4 --- /dev/null +++ b/api/README.md @@ -0,0 +1 @@ +# API proto diff --git a/api/kratos/api/annotations.pb.go b/api/kratos/api/annotations.pb.go new file mode 100644 index 000000000..a6743b84c --- /dev/null +++ b/api/kratos/api/annotations.pb.go @@ -0,0 +1,100 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.25.0 +// protoc v3.13.0 +// source: kratos/api/annotations.proto + +package api + +import ( + proto "github.com/golang/protobuf/proto" + descriptor "github.com/golang/protobuf/protoc-gen-go/descriptor" + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" + reflect "reflect" +) + +const ( + // Verify that this generated code is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) + // Verify that runtime/protoimpl is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) +) + +// This is a compile-time assertion that a sufficiently up-to-date version +// of the legacy proto package is being used. +const _ = proto.ProtoPackageIsVersion4 + +var file_kratos_api_annotations_proto_extTypes = []protoimpl.ExtensionInfo{ + { + ExtendedType: (*descriptor.EnumOptions)(nil), + ExtensionType: (*bool)(nil), + Field: 1000, + Name: "kratos.api.errors", + Tag: "varint,1000,opt,name=errors", + Filename: "kratos/api/annotations.proto", + }, +} + +// Extension fields to descriptor.EnumOptions. +var ( + // optional bool errors = 1000; + E_Errors = &file_kratos_api_annotations_proto_extTypes[0] +) + +var File_kratos_api_annotations_proto protoreflect.FileDescriptor + +var file_kratos_api_annotations_proto_rawDesc = []byte{ + 0x0a, 0x1c, 0x6b, 0x72, 0x61, 0x74, 0x6f, 0x73, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x61, 0x6e, 0x6e, + 0x6f, 0x74, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x0a, + 0x6b, 0x72, 0x61, 0x74, 0x6f, 0x73, 0x2e, 0x61, 0x70, 0x69, 0x1a, 0x20, 0x67, 0x6f, 0x6f, 0x67, + 0x6c, 0x65, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2f, 0x64, 0x65, 0x73, 0x63, + 0x72, 0x69, 0x70, 0x74, 0x6f, 0x72, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x3a, 0x35, 0x0a, 0x06, + 0x65, 0x72, 0x72, 0x6f, 0x72, 0x73, 0x12, 0x1c, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, + 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6e, 0x75, 0x6d, 0x4f, 0x70, 0x74, + 0x69, 0x6f, 0x6e, 0x73, 0x18, 0xe8, 0x07, 0x20, 0x01, 0x28, 0x08, 0x52, 0x06, 0x65, 0x72, 0x72, + 0x6f, 0x72, 0x73, 0x42, 0x58, 0x0a, 0x15, 0x63, 0x6f, 0x6d, 0x2e, 0x67, 0x69, 0x74, 0x68, 0x75, + 0x62, 0x2e, 0x6b, 0x72, 0x61, 0x74, 0x6f, 0x73, 0x2e, 0x61, 0x70, 0x69, 0x50, 0x01, 0x5a, 0x31, + 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x67, 0x6f, 0x2d, 0x6b, 0x72, + 0x61, 0x74, 0x6f, 0x73, 0x2f, 0x6b, 0x72, 0x61, 0x74, 0x6f, 0x73, 0x2f, 0x76, 0x32, 0x2f, 0x61, + 0x70, 0x69, 0x2f, 0x6b, 0x72, 0x61, 0x74, 0x6f, 0x73, 0x2f, 0x61, 0x70, 0x69, 0x3b, 0x61, 0x70, + 0x69, 0xa2, 0x02, 0x09, 0x4b, 0x72, 0x61, 0x74, 0x6f, 0x73, 0x41, 0x50, 0x49, 0x62, 0x06, 0x70, + 0x72, 0x6f, 0x74, 0x6f, 0x33, +} + +var file_kratos_api_annotations_proto_goTypes = []interface{}{ + (*descriptor.EnumOptions)(nil), // 0: google.protobuf.EnumOptions +} +var file_kratos_api_annotations_proto_depIdxs = []int32{ + 0, // 0: kratos.api.errors:extendee -> google.protobuf.EnumOptions + 1, // [1:1] is the sub-list for method output_type + 1, // [1:1] is the sub-list for method input_type + 1, // [1:1] is the sub-list for extension type_name + 0, // [0:1] is the sub-list for extension extendee + 0, // [0:0] is the sub-list for field type_name +} + +func init() { file_kratos_api_annotations_proto_init() } +func file_kratos_api_annotations_proto_init() { + if File_kratos_api_annotations_proto != nil { + return + } + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: file_kratos_api_annotations_proto_rawDesc, + NumEnums: 0, + NumMessages: 0, + NumExtensions: 1, + NumServices: 0, + }, + GoTypes: file_kratos_api_annotations_proto_goTypes, + DependencyIndexes: file_kratos_api_annotations_proto_depIdxs, + ExtensionInfos: file_kratos_api_annotations_proto_extTypes, + }.Build() + File_kratos_api_annotations_proto = out.File + file_kratos_api_annotations_proto_rawDesc = nil + file_kratos_api_annotations_proto_goTypes = nil + file_kratos_api_annotations_proto_depIdxs = nil +} diff --git a/api/kratos/api/annotations.proto b/api/kratos/api/annotations.proto new file mode 100644 index 000000000..34c4b17e6 --- /dev/null +++ b/api/kratos/api/annotations.proto @@ -0,0 +1,13 @@ +syntax = "proto3"; +package kratos.api; + +option go_package = "github.com/go-kratos/kratos/v2/api/kratos/api;api"; +option java_multiple_files = true; +option java_package = "com.github.kratos.api"; +option objc_class_prefix = "KratosAPI"; + +import "google/protobuf/descriptor.proto"; + +extend google.protobuf.EnumOptions { + bool errors = 1000; +} diff --git a/app.go b/app.go new file mode 100644 index 000000000..ebf119176 --- /dev/null +++ b/app.go @@ -0,0 +1,134 @@ +package kratos + +import ( + "context" + "errors" + "os" + "os/signal" + "syscall" + + "github.com/go-kratos/kratos/v2/log" + "github.com/go-kratos/kratos/v2/registry" + "github.com/go-kratos/kratos/v2/transport" + + "github.com/google/uuid" + "golang.org/x/sync/errgroup" +) + +// App is an application components lifecycle manager +type App struct { + opts options + ctx context.Context + cancel func() + instance *registry.ServiceInstance + log *log.Helper +} + +// New create an application lifecycle manager. +func New(opts ...Option) *App { + options := options{ + ctx: context.Background(), + logger: log.DefaultLogger, + sigs: []os.Signal{syscall.SIGTERM, syscall.SIGQUIT, syscall.SIGINT}, + } + if id, err := uuid.NewUUID(); err == nil { + options.id = id.String() + } + for _, o := range opts { + o(&options) + } + ctx, cancel := context.WithCancel(options.ctx) + return &App{ + opts: options, + ctx: ctx, + cancel: cancel, + instance: serviceInstance(options), + log: log.NewHelper("app", options.logger), + } +} + +// Logger returns logger. +func (a *App) Logger() log.Logger { + return a.opts.logger +} + +// Server returns transport servers. +func (a *App) Server() []transport.Server { + return a.opts.servers +} + +// Registry returns registry. +func (a *App) Registry() registry.Registry { + return a.opts.registry +} + +// Run executes all OnStart hooks registered with the application's Lifecycle. +func (a *App) Run() error { + a.log.Infow( + "service_id", a.opts.id, + "service_name", a.opts.name, + "version", a.opts.version, + ) + g, ctx := errgroup.WithContext(a.ctx) + for _, srv := range a.opts.servers { + srv := srv + g.Go(func() error { + <-ctx.Done() // wait for stop signal + return srv.Stop() + }) + g.Go(func() error { + return srv.Start() + }) + } + if a.opts.registry != nil { + if err := a.opts.registry.Register(a.instance); err != nil { + return err + } + } + c := make(chan os.Signal, 1) + signal.Notify(c, a.opts.sigs...) + g.Go(func() error { + for { + select { + case <-ctx.Done(): + return ctx.Err() + case <-c: + a.Stop() + } + } + }) + if err := g.Wait(); err != nil && !errors.Is(err, context.Canceled) { + return err + } + return nil +} + +// Stop gracefully stops the application. +func (a *App) Stop() error { + if a.opts.registry != nil { + if err := a.opts.registry.Deregister(a.instance); err != nil { + return err + } + } + if a.cancel != nil { + a.cancel() + } + return nil +} + +func serviceInstance(o options) *registry.ServiceInstance { + if len(o.endpoints) == 0 { + for _, srv := range o.servers { + if e, err := srv.Endpoint(); err == nil { + o.endpoints = append(o.endpoints, e) + } + } + } + return ®istry.ServiceInstance{ + ID: o.id, + Name: o.name, + Version: o.version, + Metadata: o.metadata, + Endpoints: o.endpoints, + } +} diff --git a/app_test.go b/app_test.go new file mode 100644 index 000000000..bea39401f --- /dev/null +++ b/app_test.go @@ -0,0 +1,25 @@ +package kratos + +import ( + "testing" + "time" + + "github.com/go-kratos/kratos/v2/transport/grpc" + "github.com/go-kratos/kratos/v2/transport/http" +) + +func TestApp(t *testing.T) { + hs := http.NewServer() + gs := grpc.NewServer() + app := New( + Name("kratos"), + Version("v1.0.0"), + Server(hs, gs), + ) + time.AfterFunc(time.Second, func() { + app.Stop() + }) + if err := app.Run(); err != nil { + t.Fatal(err) + } +} diff --git a/cmd/kratos/go.mod b/cmd/kratos/go.mod new file mode 100644 index 000000000..4807cef88 --- /dev/null +++ b/cmd/kratos/go.mod @@ -0,0 +1,10 @@ +module github.com/go-kratos/kratos/cmd/kratos + +go 1.15 + +require ( + github.com/emicklei/proto v1.9.0 + github.com/go-git/go-git/v5 v5.2.0 + github.com/spf13/cobra v1.1.1 + golang.org/x/mod v0.4.0 +) diff --git a/cmd/kratos/go.sum b/cmd/kratos/go.sum new file mode 100644 index 000000000..53bdfb6e9 --- /dev/null +++ b/cmd/kratos/go.sum @@ -0,0 +1,354 @@ +cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU= +cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU= +cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY= +cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc= +cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0= +cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o= +cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE= +cloud.google.com/go/firestore v1.1.0/go.mod h1:ulACoGHTpvq5r8rxGJ4ddJZBZqakUQqClKRT5SZwBmk= +cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I= +cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw= +dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= +github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= +github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= +github.com/alcortesm/tgz v0.0.0-20161220082320-9c5fe88206d7 h1:uSoVVbwJiQipAclBbw+8quDsfcvFjOpI5iCf4p/cqCs= +github.com/alcortesm/tgz v0.0.0-20161220082320-9c5fe88206d7/go.mod h1:6zEj6s6u/ghQa61ZWa/C2Aw3RkjiTBOix7dkqa1VLIs= +github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= +github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= +github.com/anmitsu/go-shlex v0.0.0-20161002113705-648efa622239 h1:kFOfPq6dUM1hTo4JG6LR5AXSUEsOjtdm0kw0FtQtMJA= +github.com/anmitsu/go-shlex v0.0.0-20161002113705-648efa622239/go.mod h1:2FmKhYUyUczH0OGQWaF5ceTx0UBShxjsH6f8oGKYe2c= +github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o= +github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY= +github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8= +github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio= +github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs= +github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= +github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= +github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs= +github.com/bketelsen/crypt v0.0.3-0.20200106085610-5cbc8cc4026c/go.mod h1:MKsuJmJgSg28kpZDP6UIiPt0e0Oz0kqKNGyRaWEPv84= +github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc= +github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= +github.com/coreos/bbolt v1.3.2/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk= +github.com/coreos/etcd v3.3.13+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE= +github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= +github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= +github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA= +github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= +github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= +github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no= +github.com/emicklei/proto v1.9.0 h1:l0QiNT6Qs7Yj0Mb4X6dnWBQer4ebei2BFcgQLbGqUDc= +github.com/emicklei/proto v1.9.0/go.mod h1:rn1FgRS/FANiZdD2djyH7TMA9jdRDcYQ9IEN9yvjX0A= +github.com/emirpasic/gods v1.12.0 h1:QAUIPSaCu4G+POclxeqb3F+WPpdKqFGlw36+yOzGlrg= +github.com/emirpasic/gods v1.12.0/go.mod h1:YfzfFFoVP/catgzJb4IKIqXjX78Ha8FMSDh3ymbK86o= +github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= +github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568 h1:BHsljHzVlRcyQhjrss6TZTdY2VfCqZPbv5k3iBFa2ZQ= +github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568/go.mod h1:xEzjJPgXI435gkrCt3MPfRiAkVrwSbHsst4LCFVfpJc= +github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= +github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= +github.com/gliderlabs/ssh v0.2.2 h1:6zsha5zo/TWhRhwqCD3+EarCAgZ2yN28ipRnGPnwkI0= +github.com/gliderlabs/ssh v0.2.2/go.mod h1:U7qILu1NlMHj9FlMhZLlkCdDnU1DBEAqr0aevW3Awn0= +github.com/go-git/gcfg v1.5.0 h1:Q5ViNfGF8zFgyJWPqYwA7qGFoMTEiBmdlkcfRmpIMa4= +github.com/go-git/gcfg v1.5.0/go.mod h1:5m20vg6GwYabIxaOonVkTdrILxQMpEShl1xiMF4ua+E= +github.com/go-git/go-billy/v5 v5.0.0 h1:7NQHvd9FVid8VL4qVUMm8XifBK+2xCoZ2lSk0agRrHM= +github.com/go-git/go-billy/v5 v5.0.0/go.mod h1:pmpqyWchKfYfrkb/UVH4otLvyi/5gJlGI4Hb3ZqZ3W0= +github.com/go-git/go-git-fixtures/v4 v4.0.2-0.20200613231340-f56387b50c12 h1:PbKy9zOy4aAKrJ5pibIRpVO2BXnK1Tlcg+caKI7Ox5M= +github.com/go-git/go-git-fixtures/v4 v4.0.2-0.20200613231340-f56387b50c12/go.mod h1:m+ICp2rF3jDhFgEZ/8yziagdT1C+ZpZcrJjappBCDSw= +github.com/go-git/go-git/v5 v5.2.0 h1:YPBLG/3UK1we1ohRkncLjaXWLW+HKp5QNM/jTli2JgI= +github.com/go-git/go-git/v5 v5.2.0/go.mod h1:kh02eMX+wdqqxgNMEyq8YgwlIOsDOa9homkUq1PoTMs= +github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= +github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= +github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= +github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= +github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= +github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= +github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4= +github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= +github.com/golang/groupcache v0.0.0-20190129154638-5b532d6fd5ef/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y= +github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= +github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= +github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= +github.com/google/go-cmp v0.3.0 h1:crn/baboCvb5fXaQ0IJ1SGTsTVrWpDsCWC8EGETZijY= +github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= +github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= +github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= +github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= +github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= +github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= +github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= +github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= +github.com/grpc-ecosystem/go-grpc-middleware v1.0.0/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs= +github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk= +github.com/grpc-ecosystem/grpc-gateway v1.9.0/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY= +github.com/hashicorp/consul/api v1.1.0/go.mod h1:VmuI/Lkw1nC05EYQWNKwWGbkg+FbDBtguAZLlVdkD9Q= +github.com/hashicorp/consul/sdk v0.1.1/go.mod h1:VKf9jXwCTEY1QZP2MOLRhb5i/I/ssyNV1vwHyQBF0x8= +github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= +github.com/hashicorp/go-cleanhttp v0.5.1/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80= +github.com/hashicorp/go-immutable-radix v1.0.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60= +github.com/hashicorp/go-msgpack v0.5.3/go.mod h1:ahLV/dePpqEmjfWmKiqvPkv/twdG7iPBM1vqhUKIvfM= +github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk= +github.com/hashicorp/go-rootcerts v1.0.0/go.mod h1:K6zTfqpRlCUIjkwsN4Z+hiSfzSTQa6eBIzfwKfwNnHU= +github.com/hashicorp/go-sockaddr v1.0.0/go.mod h1:7Xibr9yA9JjQq1JpNB2Vw7kxv8xerXegt+ozgdvDeDU= +github.com/hashicorp/go-syslog v1.0.0/go.mod h1:qPfqrKkXGihmCqbJM2mZgkZGvKG1dFdvsLplgctolz4= +github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= +github.com/hashicorp/go-uuid v1.0.1/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= +github.com/hashicorp/go.net v0.0.1/go.mod h1:hjKkEWcCURg++eb33jQU7oqQcI9XDCnUzHA0oac0k90= +github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= +github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= +github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= +github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO+LraFDTW64= +github.com/hashicorp/mdns v1.0.0/go.mod h1:tL+uN++7HEJ6SQLQ2/p+z2pH24WQKWjBPkE0mNTz8vQ= +github.com/hashicorp/memberlist v0.1.3/go.mod h1:ajVTdAv/9Im8oMAAj5G31PhhMCZJV2pPBoIllUwCN7I= +github.com/hashicorp/serf v0.8.2/go.mod h1:6hOLApaqBFA1NXqRQAsxw9QxuDEvNxSQRwA/JwenrHc= +github.com/imdario/mergo v0.3.9 h1:UauaLniWCFHWd+Jp9oCEkTBj8VO/9DKg3PV3VCNMDIg= +github.com/imdario/mergo v0.3.9/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA= +github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM= +github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= +github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOlocH6Fxy8MmwDt+yVQYULKfN0RoTN8A= +github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo3i6rXxKeerYnT8Nvf0QmHCRC1n8sfWVwXF2Frvo= +github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI= +github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo= +github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= +github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= +github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= +github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= +github.com/kevinburke/ssh_config v0.0.0-20190725054713-01f96b0aa0cd h1:Coekwdh0v2wtGp9Gmz1Ze3eVRAWJMLokvN3QjdzCHLY= +github.com/kevinburke/ssh_config v0.0.0-20190725054713-01f96b0aa0cd/go.mod h1:CT57kijsi8u/K/BOFA39wgDQJ9CxiF4nAY/ojJ6r6mM= +github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q= +github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= +github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= +github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/magiconair/properties v1.8.1/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= +github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= +github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= +github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= +github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg= +github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc= +github.com/mitchellh/go-homedir v1.0.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= +github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y= +github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= +github.com/mitchellh/go-testing-interface v1.0.0/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI= +github.com/mitchellh/gox v0.4.0/go.mod h1:Sd9lOJ0+aimLBi73mGofS1ycjY8lL3uZM3JPS42BGNg= +github.com/mitchellh/iochan v1.0.0/go.mod h1:JwYml1nuB7xOzsp52dPpHFffvOCDupsG0QubkSMEySY= +github.com/mitchellh/mapstructure v0.0.0-20160808181253-ca63d7c062ee/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= +github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= +github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= +github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs= +github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= +github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U= +github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= +github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic= +github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I= +github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI= +github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= +github.com/prometheus/client_golang v0.9.3/go.mod h1:/TN21ttK/J9q6uSwhBd54HahCDft0ttaMvbicHlPoso= +github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= +github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/prometheus/common v0.0.0-20181113130724-41aa239b4cce/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro= +github.com/prometheus/common v0.4.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= +github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= +github.com/prometheus/procfs v0.0.0-20190507164030-5867b95ac084/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= +github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU= +github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg= +github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= +github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts= +github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc= +github.com/sergi/go-diff v1.1.0 h1:we8PVUC3FE2uYfodKH/nBHMSetSfHDR6scGdBi+erh0= +github.com/sergi/go-diff v1.1.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM= +github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= +github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= +github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= +github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA= +github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM= +github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= +github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ= +github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= +github.com/spf13/cobra v1.1.1 h1:KfztREH0tPxJJ+geloSLaAkaPkr4ki2Er5quFV1TDo4= +github.com/spf13/cobra v1.1.1/go.mod h1:WnodtKOvamDL/PwE2M4iKs8aMDBZ5Q5klgD3qfVJQMI= +github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo= +github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= +github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= +github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/spf13/viper v1.7.0/go.mod h1:8WkrPz2fc9jxqZNCJI/76HCieCp4Q8HaLFoCha5qpdg= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk= +github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw= +github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= +github.com/xanzy/ssh-agent v0.2.1 h1:TCbipTQL2JiiCprBWx9frJ2eJlCYT00NmctrHxVAr70= +github.com/xanzy/ssh-agent v0.2.1/go.mod h1:mLlQY/MoOhWBj+gOGMQkOeiEvkx+8pJSI+0Bx9h2kr4= +github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU= +go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU= +go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= +go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= +go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= +go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0= +go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= +golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +golang.org/x/crypto v0.0.0-20181029021203-45a5f77698d3/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +golang.org/x/crypto v0.0.0-20190219172222-a4c6cb3142f2/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20200302210943-78000ba7a073 h1:xMPOj6Pz6UipU1wXLkrtqpHbR0AVFnyPEQq/wRWz9lM= +golang.org/x/crypto v0.0.0-20200302210943-78000ba7a073/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= +golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek= +golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY= +golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= +golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= +golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= +golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE= +golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o= +golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= +golang.org/x/mod v0.1.0 h1:sfUMP1Gu8qASkorDVjnMuvgJzwFbTZSeXFiGBYAVdl4= +golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY= +golang.org/x/mod v0.4.0 h1:8pl+sMODzuvGJkmj2W4kZihvVb5mKm8pB/X44PIQHv8= +golang.org/x/mod v0.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20181023162649-9b4f9f5ad519/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20181201002055-351d144fa1fc/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200301022130-244492dfa37a h1:GuSPYbZzB5/dcLNCwLQLsg3obCJtX9IJhpXkvY7kzk0= +golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= +golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20181026203630-95b1ffbd15a5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20181107165924-66b7b1311ac8/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190221075227-b4e8571b14e0/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527 h1:uYVVQ9WP/Ds2ROhcaGPeIdVq0RIXVLwsHlnvJ+cT1So= +golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs= +golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= +golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= +golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191112195655-aa38f8e97acc/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898 h1:/atklqdjdhuosWIl6AIbOeHJjicWYPqR9bpxqxYG2pA= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE= +google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M= +google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= +google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= +google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= +google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= +google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0= +google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= +google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= +google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= +google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8= +google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= +google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= +google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= +gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f h1:BLraFXnmrev5lT+xlilqcH8XK9/i0At2xKjWk4p6zsU= +gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= +gopkg.in/ini.v1 v1.51.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= +gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo= +gopkg.in/warnings.v0 v0.1.2 h1:wFXVbFY8DY5/xOe1ECiWdKCzZlxgshcYVNkBHstARME= +gopkg.in/warnings.v0 v0.1.2/go.mod h1:jksf8JmL6Qr/oQM2OXTHunEvvTAsrWBLb6OOjuVWRNI= +gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74= +gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10= +gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= +rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= diff --git a/cmd/kratos/internal/base/mod.go b/cmd/kratos/internal/base/mod.go new file mode 100644 index 000000000..4df815018 --- /dev/null +++ b/cmd/kratos/internal/base/mod.go @@ -0,0 +1,16 @@ +package base + +import ( + "io/ioutil" + + "golang.org/x/mod/modfile" +) + +// ModulePath returns go module path. +func ModulePath(filename string) (string, error) { + modBytes, err := ioutil.ReadFile(filename) + if err != nil { + return "", err + } + return modfile.ModulePath(modBytes), nil +} diff --git a/cmd/kratos/internal/base/path.go b/cmd/kratos/internal/base/path.go new file mode 100644 index 000000000..32c5f118d --- /dev/null +++ b/cmd/kratos/internal/base/path.go @@ -0,0 +1,100 @@ +package base + +import ( + "bytes" + "io/ioutil" + "log" + "os" + "path" +) + +func kratosHome() string { + dir, err := os.UserHomeDir() + if err != nil { + log.Fatal(err) + } + home := path.Join(dir, ".kratos") + if _, err := os.Stat(home); os.IsNotExist(err) { + if err := os.MkdirAll(home, 0700); err != nil { + log.Fatal(err) + } + } + return home +} + +func kratosHomeWithDir(dir string) string { + home := path.Join(kratosHome(), dir) + if _, err := os.Stat(home); os.IsNotExist(err) { + if err := os.MkdirAll(home, 0700); err != nil { + log.Fatal(err) + } + } + return home +} + +func copyFile(src, dst string, replaces []string) error { + var err error + srcinfo, err := os.Stat(src) + if err != nil { + return err + } + buf, err := ioutil.ReadFile(src) + if err != nil { + return err + } + var old string + for i, next := range replaces { + if i%2 == 0 { + old = next + continue + } + buf = bytes.ReplaceAll(buf, []byte(old), []byte(next)) + } + return ioutil.WriteFile(dst, buf, srcinfo.Mode()) +} + +func copyDir(src, dst string, replaces, ignores []string) error { + var err error + var fds []os.FileInfo + var srcinfo os.FileInfo + + if srcinfo, err = os.Stat(src); err != nil { + return err + } + + if err = os.MkdirAll(dst, srcinfo.Mode()); err != nil { + return err + } + + if fds, err = ioutil.ReadDir(src); err != nil { + return err + } + for _, fd := range fds { + if hasSets(fd.Name(), ignores) { + continue + } + + srcfp := path.Join(src, fd.Name()) + dstfp := path.Join(dst, fd.Name()) + + if fd.IsDir() { + if err = copyDir(srcfp, dstfp, replaces, ignores); err != nil { + return err + } + } else { + if err = copyFile(srcfp, dstfp, replaces); err != nil { + return err + } + } + } + return nil +} + +func hasSets(name string, sets []string) bool { + for _, ig := range sets { + if ig == name { + return true + } + } + return false +} diff --git a/cmd/kratos/internal/base/repo.go b/cmd/kratos/internal/base/repo.go new file mode 100644 index 000000000..8414cca13 --- /dev/null +++ b/cmd/kratos/internal/base/repo.go @@ -0,0 +1,71 @@ +package base + +import ( + "context" + "errors" + "os" + "path" + "strings" + + "github.com/go-git/go-git/v5" +) + +// Repo is git repository manager. +type Repo struct { + url string + home string +} + +// NewRepo new a repository manager. +func NewRepo(url string) *Repo { + return &Repo{ + url: url, + home: kratosHomeWithDir("repo"), + } +} + +func (r *Repo) Path() string { + start := strings.LastIndex(r.url, "/") + end := strings.LastIndex(r.url, ".git") + return path.Join(r.home, r.url[start+1:end]) +} + +func (r *Repo) Pull(ctx context.Context, url string) error { + repo, err := git.PlainOpen(r.Path()) + if err != nil { + return err + } + w, err := repo.Worktree() + if err != nil { + return err + } + if err = w.PullContext(ctx, &git.PullOptions{ + RemoteName: "origin", + Progress: os.Stdout, + }); errors.Is(err, git.NoErrAlreadyUpToDate) { + return nil + } + return err +} + +func (r *Repo) Clone(ctx context.Context) error { + if _, err := os.Stat(r.Path()); !os.IsNotExist(err) { + return r.Pull(ctx, r.url) + } + _, err := git.PlainCloneContext(ctx, r.Path(), false, &git.CloneOptions{ + URL: r.url, + Progress: os.Stdout, + }) + return err +} + +func (r *Repo) CopyTo(ctx context.Context, to string, modPath string, ignores []string) error { + if err := r.Clone(ctx); err != nil { + return err + } + mod, err := ModulePath(path.Join(r.Path(), "go.mod")) + if err != nil { + return err + } + return copyDir(r.Path(), to, []string{mod, modPath}, ignores) +} diff --git a/cmd/kratos/internal/base/repo_test.go b/cmd/kratos/internal/base/repo_test.go new file mode 100644 index 000000000..69fa17c35 --- /dev/null +++ b/cmd/kratos/internal/base/repo_test.go @@ -0,0 +1,16 @@ +package base + +import ( + "context" + "testing" +) + +func TestRepo(t *testing.T) { + r := NewRepo(RepoURL("https://github.com/go-kratos/service-layout.git")) + if err := r.Clone(context.Background()); err != nil { + t.Fatal(err) + } + if err := r.CopyTo(context.Background(), "/tmp/test_repo"); err != nil { + t.Fatal(err) + } +} diff --git a/cmd/kratos/internal/new/new.go b/cmd/kratos/internal/new/new.go new file mode 100644 index 000000000..3893c40b1 --- /dev/null +++ b/cmd/kratos/internal/new/new.go @@ -0,0 +1,36 @@ +package new + +import ( + "context" + "fmt" + "os" + "time" + + "github.com/spf13/cobra" +) + +// CmdNew represents the new command. +var CmdNew = &cobra.Command{ + Use: "new", + Short: "Create a service template", + Long: "Create a service project using the repository template. Example: kratos new helloworld", + Run: run, +} + +func run(cmd *cobra.Command, args []string) { + wd, err := os.Getwd() + if err != nil { + panic(err) + } + ctx, cancel := context.WithTimeout(context.Background(), time.Minute) + defer cancel() + if len(args) == 0 { + fmt.Fprintf(os.Stderr, "\033[31mERROR: project name is required.\033[m Example: kratos new helloworld\n") + return + } + p := &Project{Name: args[0]} + if err := p.Generate(ctx, wd); err != nil { + fmt.Fprintf(os.Stderr, "\033[31mERROR: %s\033[m\n", err) + return + } +} diff --git a/cmd/kratos/internal/new/project.go b/cmd/kratos/internal/new/project.go new file mode 100644 index 000000000..b95d869be --- /dev/null +++ b/cmd/kratos/internal/new/project.go @@ -0,0 +1,38 @@ +package new + +import ( + "context" + "fmt" + "os" + "path" + + "github.com/go-kratos/kratos/cmd/kratos/internal/base" +) + +const ( + serviceLayoutURL = "https://github.com/go-kratos/kratos-layout.git" +) + +// Project is a project template. +type Project struct { + Name string +} + +// Generate generate template project. +func (p *Project) Generate(ctx context.Context, dir string) error { + to := path.Join(dir, p.Name) + if _, err := os.Stat(to); !os.IsNotExist(err) { + return fmt.Errorf("%s already exists", p.Name) + } + fmt.Printf("Creating service %s\n", p.Name) + repo := base.NewRepo(serviceLayoutURL) + + if err := repo.CopyTo(ctx, to, p.Name, []string{".git", ".github"}); err != nil { + return err + } + os.Rename( + path.Join(to, "cmd", "server"), + path.Join(to, "cmd", p.Name), + ) + return nil +} diff --git a/cmd/kratos/internal/proto/add/add.go b/cmd/kratos/internal/proto/add/add.go new file mode 100644 index 000000000..e8fa29f34 --- /dev/null +++ b/cmd/kratos/internal/proto/add/add.go @@ -0,0 +1,65 @@ +package add + +import ( + "fmt" + "io/ioutil" + "strings" + + "github.com/spf13/cobra" + "golang.org/x/mod/modfile" +) + +// CmdAdd represents the add command. +var CmdAdd = &cobra.Command{ + Use: "add", + Short: "Add a proto API template", + Long: "Add a proto API template. Example: kratos add helloworld/v1/hello.proto", + Run: run, +} + +func run(cmd *cobra.Command, args []string) { + // kratos add helloworld/v1/helloworld.proto + input := args[0] + n := strings.LastIndex(input, "/") + path := input[:n] + fileName := input[n+1:] + pkgName := strings.ReplaceAll(path, "/", ".") + + p := &Proto{ + Name: fileName, + Path: path, + Package: pkgName, + GoPackage: goPackage(path), + JavaPackage: javaPackage(pkgName), + Service: serviceName(fileName), + } + if err := p.Generate(); err != nil { + fmt.Println(err) + return + } +} + +func modName() string { + modBytes, err := ioutil.ReadFile("go.mod") + if err != nil { + if modBytes, err = ioutil.ReadFile("../go.mod"); err != nil { + return "" + } + } + return modfile.ModulePath(modBytes) +} + +func goPackage(path string) string { + s := strings.Split(path, "/") + return modName() + "/" + path + ";" + s[len(s)-1] +} + +func javaPackage(name string) string { + return name +} + +func serviceName(name string) string { + return unexport(strings.Split(name, ".")[0]) +} + +func unexport(s string) string { return strings.ToUpper(s[:1]) + s[1:] } diff --git a/cmd/kratos/internal/proto/add/proto.go b/cmd/kratos/internal/proto/add/proto.go new file mode 100644 index 000000000..dbc082eb0 --- /dev/null +++ b/cmd/kratos/internal/proto/add/proto.go @@ -0,0 +1,41 @@ +package add + +import ( + "fmt" + "io/ioutil" + "os" + "path" +) + +// Proto is a proto generator. +type Proto struct { + Name string + Path string + Service string + Package string + GoPackage string + JavaPackage string +} + +// Generate generate a proto template. +func (p *Proto) Generate() error { + body, err := p.execute() + if err != nil { + return err + } + wd, err := os.Getwd() + if err != nil { + panic(err) + } + to := path.Join(wd, p.Path) + if _, err := os.Stat(to); os.IsNotExist(err) { + if err := os.MkdirAll(to, 0700); err != nil { + return err + } + } + name := path.Join(to, p.Name) + if _, err := os.Stat(name); !os.IsNotExist(err) { + return fmt.Errorf("%s already exists", p.Name) + } + return ioutil.WriteFile(name, []byte(body), 0644) +} diff --git a/cmd/kratos/internal/proto/add/template.go b/cmd/kratos/internal/proto/add/template.go new file mode 100644 index 000000000..805596777 --- /dev/null +++ b/cmd/kratos/internal/proto/add/template.go @@ -0,0 +1,52 @@ +package add + +import ( + "bytes" + "strings" + "text/template" +) + +const protoTemplate = ` +syntax = "proto3"; + +package {{.Package}}; + +option go_package = "{{.GoPackage}}"; +option java_multiple_files = true; +option java_package = "{{.JavaPackage}}"; + +service {{.Service}} { + rpc Create{{.Service}} (Create{{.Service}}Request) returns (Create{{.Service}}Reply); + rpc Update{{.Service}} (Update{{.Service}}Request) returns (Update{{.Service}}Reply); + rpc Delete{{.Service}} (Delete{{.Service}}Request) returns (Delete{{.Service}}Reply); + rpc Get{{.Service}} (Get{{.Service}}Request) returns (Get{{.Service}}Reply); + rpc List{{.Service}} (List{{.Service}}Request) returns (List{{.Service}}Reply); +} + +message Create{{.Service}}Request {} +message Create{{.Service}}Reply {} + +message Update{{.Service}}Request {} +message Update{{.Service}}Reply {} + +message Delete{{.Service}}Request {} +message Delete{{.Service}}Reply {} + +message Get{{.Service}}Request {} +message Get{{.Service}}Reply {} + +message List{{.Service}}Request {} +message List{{.Service}}Reply {} +` + +func (p *Proto) execute() ([]byte, error) { + buf := new(bytes.Buffer) + tmpl, err := template.New("proto").Parse(strings.TrimSpace(protoTemplate)) + if err != nil { + return nil, err + } + if err := tmpl.Execute(buf, p); err != nil { + return nil, err + } + return buf.Bytes(), nil +} diff --git a/cmd/kratos/internal/proto/proto.go b/cmd/kratos/internal/proto/proto.go new file mode 100644 index 000000000..77d2ec161 --- /dev/null +++ b/cmd/kratos/internal/proto/proto.go @@ -0,0 +1,27 @@ +package proto + +import ( + "github.com/go-kratos/kratos/cmd/kratos/internal/proto/add" + "github.com/go-kratos/kratos/cmd/kratos/internal/proto/service" + "github.com/go-kratos/kratos/cmd/kratos/internal/proto/source" + + "github.com/spf13/cobra" +) + +// CmdProto represents the proto command. +var CmdProto = &cobra.Command{ + Use: "proto", + Short: "Generate the proto files", + Long: "Generate the proto files.", + Run: run, +} + +func init() { + CmdProto.AddCommand(add.CmdAdd) + CmdProto.AddCommand(source.CmdSource) + CmdProto.AddCommand(service.CmdService) +} + +func run(cmd *cobra.Command, args []string) { + +} diff --git a/cmd/kratos/internal/proto/service/service.go b/cmd/kratos/internal/proto/service/service.go new file mode 100644 index 000000000..a2dca6276 --- /dev/null +++ b/cmd/kratos/internal/proto/service/service.go @@ -0,0 +1,86 @@ +package service + +import ( + "fmt" + "io/ioutil" + "log" + "os" + "path" + "strings" + + "github.com/emicklei/proto" + "github.com/spf13/cobra" +) + +// CmdService represents the service command. +var CmdService = &cobra.Command{ + Use: "service", + Short: "Generate the proto Service implementations", + Long: "Generate the proto Service implementations. Example: kratos proto service api/xxx.proto -target-dir=internal/service", + Run: run, +} +var targetDir string + +func init() { + CmdService.Flags().StringVarP(&targetDir, "-target-dir", "t", "internal/service", "generate target directory") +} + +func run(cmd *cobra.Command, args []string) { + if len(args) == 0 { + fmt.Fprintln(os.Stderr, "Please specify the proto file. Example: kratos proto service api/xxx.proto") + return + } + reader, err := os.Open(args[0]) + if err != nil { + log.Fatal(err) + } + defer reader.Close() + + parser := proto.NewParser(reader) + definition, err := parser.Parse() + if err != nil { + log.Fatal(err) + } + + var ( + pkg string + res []*Service + ) + proto.Walk(definition, + proto.WithOption(func(o *proto.Option) { + if o.Name == "go_package" { + pkg = strings.Split(o.Constant.Source, ";")[0] + } + }), + proto.WithService(func(s *proto.Service) { + cs := &Service{ + Package: pkg, + Service: s.Name, + } + for _, e := range s.Elements { + r, ok := e.(*proto.RPC) + if ok { + cs.Methods = append(cs.Methods, &Method{Service: s.Name, Name: r.Name, Request: r.RequestType, Reply: r.ReturnsType}) + } + } + res = append(res, cs) + }), + ) + for _, s := range res { + to := path.Join(targetDir, strings.ToLower(s.Service)+".go") + _, err := os.Stat(to) + if !os.IsNotExist(err) { + fmt.Fprintf(os.Stderr, "%s already exists\n", s.Service) + continue + } + if err = os.MkdirAll(targetDir, os.ModeDir); err != nil { + fmt.Fprintf(os.Stderr, "Failed to create file directory: %s\n", targetDir) + continue + } + b, err := s.execute() + if err != nil { + log.Fatal(err) + } + ioutil.WriteFile(to, b, 0644) + } +} diff --git a/cmd/kratos/internal/proto/service/template.go b/cmd/kratos/internal/proto/service/template.go new file mode 100644 index 000000000..b2f255187 --- /dev/null +++ b/cmd/kratos/internal/proto/service/template.go @@ -0,0 +1,56 @@ +package service + +import ( + "bytes" + "html/template" +) + +var serviceTemplate = ` +package service + +import( + "context" + + pb "{{.Package}}" +) + +type {{.Service}}Service struct { + pb.Unimplemented{{.Service}}Server +} + +func New{{.Service}}Service() pb.{{.Service}}Server { + return &{{.Service}}Service{} +} +{{ range .Methods }} +func (s *{{.Service}}Service) {{.Name}}(ctx context.Context, req *pb.{{.Request}}) (*pb.{{.Reply}}, error) { + return &pb.{{.Reply}}{}, nil +} +{{- end }} +` + +// Service is a proto service. +type Service struct { + Package string + Service string + Methods []*Method +} + +// Method is a proto method. +type Method struct { + Service string + Name string + Request string + Reply string +} + +func (s *Service) execute() ([]byte, error) { + buf := new(bytes.Buffer) + tmpl, err := template.New("service").Parse(serviceTemplate) + if err != nil { + return nil, err + } + if err := tmpl.Execute(buf, s); err != nil { + return nil, err + } + return buf.Bytes(), nil +} diff --git a/cmd/kratos/internal/proto/source/source.go b/cmd/kratos/internal/proto/source/source.go new file mode 100644 index 000000000..799056de5 --- /dev/null +++ b/cmd/kratos/internal/proto/source/source.go @@ -0,0 +1,28 @@ +package source + +import ( + "fmt" + "log" + "os/exec" + + "github.com/spf13/cobra" +) + +// CmdSource represents the source command. +var CmdSource = &cobra.Command{ + Use: "source", + Short: "Generate the proto source code", + Long: "Generate the proto source code. Example: kratos proto source ./**/*.proto", + Run: run, +} + +func run(cmd *cobra.Command, args []string) { + input := []string{"--go_out=paths=source_relative:.", "--go-grpc_out=paths=source_relative:."} + input = append(input, args...) + do := exec.Command("protoc", input...) + out, err := do.CombinedOutput() + if err != nil { + log.Fatalf("failed to execute: %s\n", err) + } + fmt.Println(string(out)) +} diff --git a/cmd/kratos/main.go b/cmd/kratos/main.go new file mode 100644 index 000000000..77c3eec6d --- /dev/null +++ b/cmd/kratos/main.go @@ -0,0 +1,27 @@ +package main + +import ( + "log" + + "github.com/go-kratos/kratos/cmd/kratos/internal/new" + "github.com/go-kratos/kratos/cmd/kratos/internal/proto" + "github.com/spf13/cobra" +) + +var rootCmd = &cobra.Command{ + Use: "kratos", + Short: "Kratos: An elegant toolkit for Go microservices.", + Long: `Kratos: An elegant toolkit for Go microservices.`, + Version: Version, +} + +func init() { + rootCmd.AddCommand(new.CmdNew) + rootCmd.AddCommand(proto.CmdProto) +} + +func main() { + if err := rootCmd.Execute(); err != nil { + log.Fatal(err) + } +} diff --git a/cmd/kratos/version.go b/cmd/kratos/version.go new file mode 100644 index 000000000..8d657404f --- /dev/null +++ b/cmd/kratos/version.go @@ -0,0 +1,13 @@ +package main + +// go build -ldflags "-X main.Version=x.y.yz" +var ( + // Version is the version of the compiled software. + Version string = "v2.0.0" + // Branch is current branch name the code is built off + Branch string + // Revision is the short commit hash of source tree + Revision string + // BuildDate is the date when the binary was built. + BuildDate string +) diff --git a/cmd/protoc-gen-go-errors/errors.go b/cmd/protoc-gen-go-errors/errors.go new file mode 100644 index 000000000..081b132eb --- /dev/null +++ b/cmd/protoc-gen-go-errors/errors.go @@ -0,0 +1,58 @@ +package main + +import ( + pb "github.com/go-kratos/kratos/v2/api/kratos/api" + + "google.golang.org/protobuf/compiler/protogen" + "google.golang.org/protobuf/proto" +) + +const ( + errorsPackage = protogen.GoImportPath("github.com/go-kratos/kratos/v2/errors") +) + +// generateFile generates a _http.pb.go file containing kratos errors definitions. +func generateFile(gen *protogen.Plugin, file *protogen.File) *protogen.GeneratedFile { + if len(file.Enums) == 0 { + return nil + } + filename := file.GeneratedFilenamePrefix + "_errors.pb.go" + g := gen.NewGeneratedFile(filename, file.GoImportPath) + g.P("// Code generated by protoc-gen-go-errors. DO NOT EDIT.") + g.P() + g.P("package ", file.GoPackageName) + g.P() + generateFileContent(gen, file, g) + return g +} + +// generateFileContent generates the kratos errors definitions, excluding the package statement. +func generateFileContent(gen *protogen.Plugin, file *protogen.File, g *protogen.GeneratedFile) { + if len(file.Enums) == 0 { + return + } + + g.P("// This is a compile-time assertion to ensure that this generated file") + g.P("// is compatible with the kratos package it is being compiled against.") + g.P("const _ = ", errorsPackage.Ident("SupportPackageIsVersion1")) + g.P() + for _, enum := range file.Enums { + genErrorsReason(gen, file, g, enum) + } +} + +func genErrorsReason(gen *protogen.Plugin, file *protogen.File, g *protogen.GeneratedFile, enum *protogen.Enum) { + err := proto.GetExtension(enum.Desc.Options(), pb.E_Errors) + if ok := err.(bool); !ok { + return + } + var ew errorWrapper + for _, v := range enum.Values { + err := &errorInfo{ + Name: string(enum.Desc.Name()), + Value: string(v.Desc.Name()), + } + ew.Errors = append(ew.Errors, err) + } + g.P(ew.execute()) +} diff --git a/cmd/protoc-gen-go-errors/go.mod b/cmd/protoc-gen-go-errors/go.mod new file mode 100644 index 000000000..bb42ca06f --- /dev/null +++ b/cmd/protoc-gen-go-errors/go.mod @@ -0,0 +1,9 @@ +module github.com/go-kratos/kratos/cmd/protoc-gen-go-errors + +go 1.15 + +require ( + github.com/go-kratos/kratos/v2 v2.0.0-20210217083752-d86d233d93ce + github.com/golang/protobuf v1.4.3 + google.golang.org/protobuf v1.25.0 +) diff --git a/cmd/protoc-gen-go-errors/go.sum b/cmd/protoc-gen-go-errors/go.sum new file mode 100644 index 000000000..c1d1b7f04 --- /dev/null +++ b/cmd/protoc-gen-go-errors/go.sum @@ -0,0 +1,101 @@ +cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= +github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= +github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= +github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= +github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= +github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= +github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= +github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= +github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= +github.com/go-kratos/kratos v0.6.0 h1:aGuIQQoj1EiWtBCIaPHvhPBcDx3WfL/Mw6q+5C5ehgg= +github.com/go-kratos/kratos/v2 v2.0.0-20201205165131-10618a745c96 h1:Qyiy167FmsHZucnc4vZ+hv938YyNTkodXVgb5PC2Y+U= +github.com/go-kratos/kratos/v2 v2.0.0-20201205165131-10618a745c96/go.mod h1:YwgE84UUomqngCzthva1VOYcuIZqXtYo5nTu22i1nbo= +github.com/go-kratos/kratos/v2 v2.0.0-20201205170920-f2b7f99a678c h1:V4dxjxZX4QEp8vAtumiA/6ibbqU9XZUUTA81piwk8nc= +github.com/go-kratos/kratos/v2 v2.0.0-20201205170920-f2b7f99a678c/go.mod h1:YwgE84UUomqngCzthva1VOYcuIZqXtYo5nTu22i1nbo= +github.com/go-kratos/kratos/v2 v2.0.0-20210207074933-00633b4860e1 h1:/blwKtlboosqLB9p+oICb8LkS13Ph0aP8V4rTil8pxE= +github.com/go-kratos/kratos/v2 v2.0.0-20210214123813-9afce08b3687 h1:3CVMCLzegnkCewvFoPBKqaYgmXG2zPCeV7Nro/JVyTk= +github.com/go-kratos/kratos/v2 v2.0.0-20210214172044-a42fa7820493 h1:39X3q7mGWL9yYZl9jwbHjLHzlsZlYajKQdewdignTVU= +github.com/go-kratos/kratos/v2 v2.0.0-20210214172044-a42fa7820493/go.mod h1:MIIjRu+ZXyn7IYlM1TBHbr75RLZOZM/CBBjT6luWL+Q= +github.com/go-kratos/kratos/v2 v2.0.0-20210217083752-d86d233d93ce h1:LfOsLN9s8tAxR8xIZGWQvEVWxHfipTnBSE0dvG4h3k8= +github.com/go-kratos/kratos/v2 v2.0.0-20210217083752-d86d233d93ce/go.mod h1:oLvFyDBJkkWN8TPqb+NmpvRrSy9uM/K+XQubVRc11a8= +github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= +github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= +github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= +github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= +github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= +github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= +github.com/golang/protobuf v1.4.1 h1:ZFgWrT+bLgsYPirOnRfKLYJLvssAegOj/hgyMFdJZe0= +github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= +github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= +github.com/golang/protobuf v1.4.3 h1:JjCZWpVbqXDqFVmTfYWEVTMIYrL/NPdPSCHPJ0T/raM= +github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= +github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= +github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So= +github.com/imdario/mergo v0.3.6/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA= +github.com/opentracing/opentracing-go v1.2.0/go.mod h1:GxEUsuufX4nBwe+T+Wl9TAgYrxe9dPLANfrWvHYVTgc= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= +golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= +golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= +golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= +google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= +google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= +google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= +google.golang.org/genproto v0.0.0-20210114201628-6edceaf6022f/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= +google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= +google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= +google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc= +google.golang.org/grpc v1.35.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= +google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= +google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= +google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= +google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= +google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= +google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4= +google.golang.org/protobuf v1.25.0 h1:Ejskq+SyPohKW+1uil0JJMtmHCgJPJ/qWTxr8qp+R4c= +google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= diff --git a/cmd/protoc-gen-go-errors/main.go b/cmd/protoc-gen-go-errors/main.go new file mode 100644 index 000000000..e590c0bc5 --- /dev/null +++ b/cmd/protoc-gen-go-errors/main.go @@ -0,0 +1,35 @@ +package main + +import ( + "flag" + "fmt" + + "google.golang.org/protobuf/compiler/protogen" + "google.golang.org/protobuf/types/pluginpb" +) + +const version = "0.0.1" + +func main() { + showVersion := flag.Bool("version", false, "print the version and exit") + flag.Parse() + if *showVersion { + fmt.Printf("protoc-gen-go-errors %v\n", version) + return + } + + var flags flag.FlagSet + + protogen.Options{ + ParamFunc: flags.Set, + }.Run(func(gen *protogen.Plugin) error { + gen.SupportedFeatures = uint64(pluginpb.CodeGeneratorResponse_FEATURE_PROTO3_OPTIONAL) + for _, f := range gen.Files { + if !f.Generate { + continue + } + generateFile(gen, f) + } + return nil + }) +} diff --git a/cmd/protoc-gen-go-errors/template.go b/cmd/protoc-gen-go-errors/template.go new file mode 100644 index 000000000..1d86bb1d2 --- /dev/null +++ b/cmd/protoc-gen-go-errors/template.go @@ -0,0 +1,40 @@ +package main + +import ( + "bytes" + "text/template" +) + +var errorsTemplate = `const ( +{{ range .Errors }} + Errors_{{.Value}} = "{{.Name}}_{{.Value}}" +{{- end }} +) + +{{ range .Errors }} + +func Is{{.Value}}(err error) bool { + return errors.Reason(err) == Errors_{{.Value}} +} +{{- end }} +` + +type errorInfo struct { + Name string + Value string +} +type errorWrapper struct { + Errors []*errorInfo +} + +func (e *errorWrapper) execute() string { + buf := new(bytes.Buffer) + tmpl, err := template.New("errors").Parse(errorsTemplate) + if err != nil { + panic(err) + } + if err := tmpl.Execute(buf, e); err != nil { + panic(err) + } + return string(buf.Bytes()) +} diff --git a/cmd/protoc-gen-go-http/go.mod b/cmd/protoc-gen-go-http/go.mod new file mode 100644 index 000000000..e7cfe961e --- /dev/null +++ b/cmd/protoc-gen-go-http/go.mod @@ -0,0 +1,11 @@ +module github.com/go-kratos/kratos/cmd/protoc-gen-go-http + +go 1.15 + +require ( + github.com/go-kratos/kratos/v2 v2.0.0-20210217083752-d86d233d93ce + github.com/golang/protobuf v1.4.3 + google.golang.org/genproto v0.0.0-20210202153253-cf70463f6119 + google.golang.org/grpc v1.35.0 + google.golang.org/protobuf v1.25.0 +) diff --git a/cmd/protoc-gen-go-http/go.sum b/cmd/protoc-gen-go-http/go.sum new file mode 100644 index 000000000..1ae024583 --- /dev/null +++ b/cmd/protoc-gen-go-http/go.sum @@ -0,0 +1,99 @@ +cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= +github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= +github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= +github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= +github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= +github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= +github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= +github.com/go-kratos/kratos/v2 v2.0.0-20210210104528-47c8db1163db h1:p5pejan0Lbn7e43dQCUTzqOswWJqLIE5lMU5TUbwCt4= +github.com/go-kratos/kratos/v2 v2.0.0-20210210104528-47c8db1163db/go.mod h1:MIIjRu+ZXyn7IYlM1TBHbr75RLZOZM/CBBjT6luWL+Q= +github.com/go-kratos/kratos/v2 v2.0.0-20210211132943-131c3975fc3d/go.mod h1:MIIjRu+ZXyn7IYlM1TBHbr75RLZOZM/CBBjT6luWL+Q= +github.com/go-kratos/kratos/v2 v2.0.0-20210217083752-d86d233d93ce/go.mod h1:oLvFyDBJkkWN8TPqb+NmpvRrSy9uM/K+XQubVRc11a8= +github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= +github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= +github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= +github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= +github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= +github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= +github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= +github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= +github.com/golang/protobuf v1.4.3 h1:JjCZWpVbqXDqFVmTfYWEVTMIYrL/NPdPSCHPJ0T/raM= +github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= +github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= +github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.0 h1:/QaMHBdZ26BB3SSst0Iwl10Epc+xhTquomWX0oZEB6w= +github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI= +github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So= +github.com/imdario/mergo v0.3.6/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA= +github.com/opentracing/opentracing-go v1.2.0/go.mod h1:GxEUsuufX4nBwe+T+Wl9TAgYrxe9dPLANfrWvHYVTgc= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= +golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190311183353-d8887717615a h1:oWX7TPOiFAMXLq8o0ikBYfCJVlRHBcsciT5bXOrH628= +golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= +golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9 h1:L2auWcuQIvxz9xSEqzESnV/QN/gNRXNApHi3fYwl2w0= +golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= +golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= +google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= +google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= +google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= +google.golang.org/genproto v0.0.0-20210114201628-6edceaf6022f/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20210202153253-cf70463f6119 h1:m9+RjTMas6brUP8DBxSAa/WIPFy7FIhKpvk+9Ppce8E= +google.golang.org/genproto v0.0.0-20210202153253-cf70463f6119/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= +google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= +google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= +google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/grpc v1.35.0 h1:TwIQcH3es+MojMVojxxfQ3l3OF2KzlRxML2xZq0kRo8= +google.golang.org/grpc v1.35.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= +google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= +google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= +google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= +google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= +google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= +google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4= +google.golang.org/protobuf v1.25.0 h1:Ejskq+SyPohKW+1uil0JJMtmHCgJPJ/qWTxr8qp+R4c= +google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= diff --git a/cmd/protoc-gen-go-http/http.go b/cmd/protoc-gen-go-http/http.go new file mode 100644 index 000000000..d03f75306 --- /dev/null +++ b/cmd/protoc-gen-go-http/http.go @@ -0,0 +1,210 @@ +package main + +import ( + "fmt" + "strings" + + "google.golang.org/genproto/googleapis/api/annotations" + "google.golang.org/protobuf/compiler/protogen" + "google.golang.org/protobuf/proto" + "google.golang.org/protobuf/types/descriptorpb" +) + +const ( + contextPackage = protogen.GoImportPath("context") + httpPackage = protogen.GoImportPath("net/http") + transportPackage = protogen.GoImportPath("github.com/go-kratos/kratos/v2/transport/http") + middlewarePackage = protogen.GoImportPath("github.com/go-kratos/kratos/v2/middleware") +) + +var methodSets = make(map[string]int) + +// generateFile generates a _http.pb.go file containing kratos errors definitions. +func generateFile(gen *protogen.Plugin, file *protogen.File) *protogen.GeneratedFile { + if len(file.Services) == 0 { + return nil + } + filename := file.GeneratedFilenamePrefix + "_http.pb.go" + g := gen.NewGeneratedFile(filename, file.GoImportPath) + g.P("// Code generated by protoc-gen-go-http. DO NOT EDIT.") + g.P() + g.P("package ", file.GoPackageName) + g.P() + generateFileContent(gen, file, g) + return g +} + +// generateFileContent generates the kratos errors definitions, excluding the package statement. +func generateFileContent(gen *protogen.Plugin, file *protogen.File, g *protogen.GeneratedFile) { + if len(file.Services) == 0 { + return + } + g.P("// This is a compile-time assertion to ensure that this generated file") + g.P("// is compatible with the kratos package it is being compiled against.") + g.P("// ", contextPackage.Ident(""), "/", httpPackage.Ident(""), "/", middlewarePackage.Ident("")) + g.P("const _ = ", transportPackage.Ident("SupportPackageIsVersion1")) + g.P() + + for _, service := range file.Services { + genService(gen, file, g, service) + } +} + +func genService(gen *protogen.Plugin, file *protogen.File, g *protogen.GeneratedFile, service *protogen.Service) { + if service.Desc.Options().(*descriptorpb.ServiceOptions).GetDeprecated() { + g.P("//") + g.P(deprecationComment) + } + // HTTP Server. + sd := &serviceDesc{ + ServiceType: service.GoName, + ServiceName: string(service.Desc.FullName()), + Metadata: file.Desc.Path(), + } + for _, method := range service.Methods { + rule, ok := proto.GetExtension(method.Desc.Options(), annotations.E_Http).(*annotations.HttpRule) + if rule != nil && ok { + for _, bind := range rule.AdditionalBindings { + sd.Methods = append(sd.Methods, buildHTTPRule(method, bind)) + } + sd.Methods = append(sd.Methods, buildHTTPRule(method, rule)) + } else { + path := fmt.Sprintf("/%s/%s", service.Desc.FullName(), method.Desc.Name()) + sd.Methods = append(sd.Methods, buildMethodDesc(method, "POST", path)) + + } + } + g.P(sd.execute()) +} + +func buildHTTPRule(m *protogen.Method, rule *annotations.HttpRule) *methodDesc { + var ( + path string + method string + body string + responseBody string + ) + switch pattern := rule.Pattern.(type) { + case *annotations.HttpRule_Get: + path = pattern.Get + method = "GET" + case *annotations.HttpRule_Put: + path = pattern.Put + method = "PUT" + case *annotations.HttpRule_Post: + path = pattern.Post + method = "POST" + case *annotations.HttpRule_Delete: + path = pattern.Delete + method = "DELETE" + case *annotations.HttpRule_Patch: + path = pattern.Patch + method = "PATCH" + case *annotations.HttpRule_Custom: + path = pattern.Custom.Path + method = pattern.Custom.Kind + } + body = rule.Body + responseBody = rule.ResponseBody + md := buildMethodDesc(m, method, path) + if body != "" { + md.Body = "." + camelCaseVars(body) + } + if responseBody != "" { + md.ResponseBody = "." + camelCaseVars(responseBody) + } + return md +} + +func buildMethodDesc(m *protogen.Method, method, path string) *methodDesc { + defer func() { methodSets[m.GoName]++ }() + return &methodDesc{ + Name: m.GoName, + Num: methodSets[m.GoName], + Request: m.Input.GoIdent.GoName, + Reply: m.Output.GoIdent.GoName, + Path: path, + Method: method, + Vars: buildPathVars(m, path), + } +} + +func buildPathVars(method *protogen.Method, path string) (res []string) { + for _, v := range strings.Split(path, "/") { + if strings.HasPrefix(v, "{") && strings.HasSuffix(v, "}") { + name := strings.TrimRight(strings.TrimLeft(v, "{"), "}") + res = append(res, name) + } + } + return +} + +func camelCaseVars(s string) string { + var ( + vars []string + subs = strings.Split(s, ".") + ) + for _, sub := range subs { + vars = append(vars, camelCase(sub)) + } + return strings.Join(vars, ".") +} + +// camelCase returns the CamelCased name. +// If there is an interior underscore followed by a lower case letter, +// drop the underscore and convert the letter to upper case. +// There is a remote possibility of this rewrite causing a name collision, +// but it's so remote we're prepared to pretend it's nonexistent - since the +// C++ generator lowercases names, it's extremely unlikely to have two fields +// with different capitalizations. +// In short, _my_field_name_2 becomes XMyFieldName_2. +func camelCase(s string) string { + if s == "" { + return "" + } + t := make([]byte, 0, 32) + i := 0 + if s[0] == '_' { + // Need a capital letter; drop the '_'. + t = append(t, 'X') + i++ + } + // Invariant: if the next letter is lower case, it must be converted + // to upper case. + // That is, we process a word at a time, where words are marked by _ or + // upper case letter. Digits are treated as words. + for ; i < len(s); i++ { + c := s[i] + if c == '_' && i+1 < len(s) && isASCIILower(s[i+1]) { + continue // Skip the underscore in s. + } + if isASCIIDigit(c) { + t = append(t, c) + continue + } + // Assume we have a letter now - if not, it's a bogus identifier. + // The next word is a sequence of characters that must start upper case. + if isASCIILower(c) { + c ^= ' ' // Make it a capital letter. + } + t = append(t, c) // Guaranteed not lower case. + // Accept lower case sequence that follows. + for i+1 < len(s) && isASCIILower(s[i+1]) { + i++ + t = append(t, s[i]) + } + } + return string(t) +} + +// Is c an ASCII lower-case letter? +func isASCIILower(c byte) bool { + return 'a' <= c && c <= 'z' +} + +// Is c an ASCII digit? +func isASCIIDigit(c byte) bool { + return '0' <= c && c <= '9' +} + +const deprecationComment = "// Deprecated: Do not use." diff --git a/cmd/protoc-gen-go-http/internal/testproto/echo_service.pb.go b/cmd/protoc-gen-go-http/internal/testproto/echo_service.pb.go new file mode 100644 index 000000000..5820c3787 --- /dev/null +++ b/cmd/protoc-gen-go-http/internal/testproto/echo_service.pb.go @@ -0,0 +1,669 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.25.0 +// protoc v3.13.0 +// source: echo_service.proto + +package testproto + +import ( + proto "github.com/golang/protobuf/proto" + _struct "github.com/golang/protobuf/ptypes/struct" + _ "google.golang.org/genproto/googleapis/api/annotations" + field_mask "google.golang.org/genproto/protobuf/field_mask" + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" + reflect "reflect" + sync "sync" +) + +const ( + // Verify that this generated code is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) + // Verify that runtime/protoimpl is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) +) + +// This is a compile-time assertion that a sufficiently up-to-date version +// of the legacy proto package is being used. +const _ = proto.ProtoPackageIsVersion4 + +type Corpus int32 + +const ( + Corpus_UNIVERSAL Corpus = 0 + Corpus_WEB Corpus = 1 + Corpus_IMAGES Corpus = 2 + Corpus_LOCAL Corpus = 3 + Corpus_NEWS Corpus = 4 + Corpus_PRODUCTS Corpus = 5 + Corpus_VIDEO Corpus = 6 +) + +// Enum value maps for Corpus. +var ( + Corpus_name = map[int32]string{ + 0: "UNIVERSAL", + 1: "WEB", + 2: "IMAGES", + 3: "LOCAL", + 4: "NEWS", + 5: "PRODUCTS", + 6: "VIDEO", + } + Corpus_value = map[string]int32{ + "UNIVERSAL": 0, + "WEB": 1, + "IMAGES": 2, + "LOCAL": 3, + "NEWS": 4, + "PRODUCTS": 5, + "VIDEO": 6, + } +) + +func (x Corpus) Enum() *Corpus { + p := new(Corpus) + *p = x + return p +} + +func (x Corpus) String() string { + return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) +} + +func (Corpus) Descriptor() protoreflect.EnumDescriptor { + return file_echo_service_proto_enumTypes[0].Descriptor() +} + +func (Corpus) Type() protoreflect.EnumType { + return &file_echo_service_proto_enumTypes[0] +} + +func (x Corpus) Number() protoreflect.EnumNumber { + return protoreflect.EnumNumber(x) +} + +// Deprecated: Use Corpus.Descriptor instead. +func (Corpus) EnumDescriptor() ([]byte, []int) { + return file_echo_service_proto_rawDescGZIP(), []int{0} +} + +// Embedded represents a message embedded in SimpleMessage. +type Embedded struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + // Types that are assignable to Mark: + // *Embedded_Progress + // *Embedded_Note + Mark isEmbedded_Mark `protobuf_oneof:"mark"` +} + +func (x *Embedded) Reset() { + *x = Embedded{} + if protoimpl.UnsafeEnabled { + mi := &file_echo_service_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *Embedded) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Embedded) ProtoMessage() {} + +func (x *Embedded) ProtoReflect() protoreflect.Message { + mi := &file_echo_service_proto_msgTypes[0] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use Embedded.ProtoReflect.Descriptor instead. +func (*Embedded) Descriptor() ([]byte, []int) { + return file_echo_service_proto_rawDescGZIP(), []int{0} +} + +func (m *Embedded) GetMark() isEmbedded_Mark { + if m != nil { + return m.Mark + } + return nil +} + +func (x *Embedded) GetProgress() int64 { + if x, ok := x.GetMark().(*Embedded_Progress); ok { + return x.Progress + } + return 0 +} + +func (x *Embedded) GetNote() string { + if x, ok := x.GetMark().(*Embedded_Note); ok { + return x.Note + } + return "" +} + +type isEmbedded_Mark interface { + isEmbedded_Mark() +} + +type Embedded_Progress struct { + Progress int64 `protobuf:"varint,1,opt,name=progress,proto3,oneof"` +} + +type Embedded_Note struct { + Note string `protobuf:"bytes,2,opt,name=note,proto3,oneof"` +} + +func (*Embedded_Progress) isEmbedded_Mark() {} + +func (*Embedded_Note) isEmbedded_Mark() {} + +// SimpleMessage represents a simple message sent to the Echo service. +type SimpleMessage struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + // Id represents the message identifier. + Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"` + Num int64 `protobuf:"varint,2,opt,name=num,proto3" json:"num,omitempty"` + // Types that are assignable to Code: + // *SimpleMessage_LineNum + // *SimpleMessage_Lang + Code isSimpleMessage_Code `protobuf_oneof:"code"` + Status *Embedded `protobuf:"bytes,5,opt,name=status,proto3" json:"status,omitempty"` + // Types that are assignable to Ext: + // *SimpleMessage_En + // *SimpleMessage_No + Ext isSimpleMessage_Ext `protobuf_oneof:"ext"` + Corpus Corpus `protobuf:"varint,8,opt,name=corpus,proto3,enum=testproto.Corpus" json:"corpus,omitempty"` +} + +func (x *SimpleMessage) Reset() { + *x = SimpleMessage{} + if protoimpl.UnsafeEnabled { + mi := &file_echo_service_proto_msgTypes[1] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *SimpleMessage) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*SimpleMessage) ProtoMessage() {} + +func (x *SimpleMessage) ProtoReflect() protoreflect.Message { + mi := &file_echo_service_proto_msgTypes[1] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use SimpleMessage.ProtoReflect.Descriptor instead. +func (*SimpleMessage) Descriptor() ([]byte, []int) { + return file_echo_service_proto_rawDescGZIP(), []int{1} +} + +func (x *SimpleMessage) GetId() string { + if x != nil { + return x.Id + } + return "" +} + +func (x *SimpleMessage) GetNum() int64 { + if x != nil { + return x.Num + } + return 0 +} + +func (m *SimpleMessage) GetCode() isSimpleMessage_Code { + if m != nil { + return m.Code + } + return nil +} + +func (x *SimpleMessage) GetLineNum() int64 { + if x, ok := x.GetCode().(*SimpleMessage_LineNum); ok { + return x.LineNum + } + return 0 +} + +func (x *SimpleMessage) GetLang() string { + if x, ok := x.GetCode().(*SimpleMessage_Lang); ok { + return x.Lang + } + return "" +} + +func (x *SimpleMessage) GetStatus() *Embedded { + if x != nil { + return x.Status + } + return nil +} + +func (m *SimpleMessage) GetExt() isSimpleMessage_Ext { + if m != nil { + return m.Ext + } + return nil +} + +func (x *SimpleMessage) GetEn() int64 { + if x, ok := x.GetExt().(*SimpleMessage_En); ok { + return x.En + } + return 0 +} + +func (x *SimpleMessage) GetNo() *Embedded { + if x, ok := x.GetExt().(*SimpleMessage_No); ok { + return x.No + } + return nil +} + +func (x *SimpleMessage) GetCorpus() Corpus { + if x != nil { + return x.Corpus + } + return Corpus_UNIVERSAL +} + +type isSimpleMessage_Code interface { + isSimpleMessage_Code() +} + +type SimpleMessage_LineNum struct { + LineNum int64 `protobuf:"varint,3,opt,name=line_num,json=lineNum,proto3,oneof"` +} + +type SimpleMessage_Lang struct { + Lang string `protobuf:"bytes,4,opt,name=lang,proto3,oneof"` +} + +func (*SimpleMessage_LineNum) isSimpleMessage_Code() {} + +func (*SimpleMessage_Lang) isSimpleMessage_Code() {} + +type isSimpleMessage_Ext interface { + isSimpleMessage_Ext() +} + +type SimpleMessage_En struct { + En int64 `protobuf:"varint,6,opt,name=en,proto3,oneof"` +} + +type SimpleMessage_No struct { + No *Embedded `protobuf:"bytes,7,opt,name=no,proto3,oneof"` +} + +func (*SimpleMessage_En) isSimpleMessage_Ext() {} + +func (*SimpleMessage_No) isSimpleMessage_Ext() {} + +// DynamicMessage represents a message which can have its structure +// built dynamically using Struct and Values. +type DynamicMessage struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + StructField *_struct.Struct `protobuf:"bytes,1,opt,name=struct_field,json=structField,proto3" json:"struct_field,omitempty"` + ValueField *_struct.Value `protobuf:"bytes,2,opt,name=value_field,json=valueField,proto3" json:"value_field,omitempty"` +} + +func (x *DynamicMessage) Reset() { + *x = DynamicMessage{} + if protoimpl.UnsafeEnabled { + mi := &file_echo_service_proto_msgTypes[2] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *DynamicMessage) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*DynamicMessage) ProtoMessage() {} + +func (x *DynamicMessage) ProtoReflect() protoreflect.Message { + mi := &file_echo_service_proto_msgTypes[2] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use DynamicMessage.ProtoReflect.Descriptor instead. +func (*DynamicMessage) Descriptor() ([]byte, []int) { + return file_echo_service_proto_rawDescGZIP(), []int{2} +} + +func (x *DynamicMessage) GetStructField() *_struct.Struct { + if x != nil { + return x.StructField + } + return nil +} + +func (x *DynamicMessage) GetValueField() *_struct.Value { + if x != nil { + return x.ValueField + } + return nil +} + +type DynamicMessageUpdate struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Body *DynamicMessage `protobuf:"bytes,1,opt,name=body,proto3" json:"body,omitempty"` + UpdateMask *field_mask.FieldMask `protobuf:"bytes,2,opt,name=update_mask,json=updateMask,proto3" json:"update_mask,omitempty"` +} + +func (x *DynamicMessageUpdate) Reset() { + *x = DynamicMessageUpdate{} + if protoimpl.UnsafeEnabled { + mi := &file_echo_service_proto_msgTypes[3] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *DynamicMessageUpdate) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*DynamicMessageUpdate) ProtoMessage() {} + +func (x *DynamicMessageUpdate) ProtoReflect() protoreflect.Message { + mi := &file_echo_service_proto_msgTypes[3] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use DynamicMessageUpdate.ProtoReflect.Descriptor instead. +func (*DynamicMessageUpdate) Descriptor() ([]byte, []int) { + return file_echo_service_proto_rawDescGZIP(), []int{3} +} + +func (x *DynamicMessageUpdate) GetBody() *DynamicMessage { + if x != nil { + return x.Body + } + return nil +} + +func (x *DynamicMessageUpdate) GetUpdateMask() *field_mask.FieldMask { + if x != nil { + return x.UpdateMask + } + return nil +} + +var File_echo_service_proto protoreflect.FileDescriptor + +var file_echo_service_proto_rawDesc = []byte{ + 0x0a, 0x12, 0x65, 0x63, 0x68, 0x6f, 0x5f, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x2e, 0x70, + 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x09, 0x74, 0x65, 0x73, 0x74, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x1a, + 0x1c, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x61, 0x6e, 0x6e, 0x6f, + 0x74, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x1a, 0x20, 0x67, + 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2f, 0x66, + 0x69, 0x65, 0x6c, 0x64, 0x5f, 0x6d, 0x61, 0x73, 0x6b, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x1a, + 0x1c, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, + 0x2f, 0x73, 0x74, 0x72, 0x75, 0x63, 0x74, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0x46, 0x0a, + 0x08, 0x45, 0x6d, 0x62, 0x65, 0x64, 0x64, 0x65, 0x64, 0x12, 0x1c, 0x0a, 0x08, 0x70, 0x72, 0x6f, + 0x67, 0x72, 0x65, 0x73, 0x73, 0x18, 0x01, 0x20, 0x01, 0x28, 0x03, 0x48, 0x00, 0x52, 0x08, 0x70, + 0x72, 0x6f, 0x67, 0x72, 0x65, 0x73, 0x73, 0x12, 0x14, 0x0a, 0x04, 0x6e, 0x6f, 0x74, 0x65, 0x18, + 0x02, 0x20, 0x01, 0x28, 0x09, 0x48, 0x00, 0x52, 0x04, 0x6e, 0x6f, 0x74, 0x65, 0x42, 0x06, 0x0a, + 0x04, 0x6d, 0x61, 0x72, 0x6b, 0x22, 0x84, 0x02, 0x0a, 0x0d, 0x53, 0x69, 0x6d, 0x70, 0x6c, 0x65, + 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, + 0x01, 0x28, 0x09, 0x52, 0x02, 0x69, 0x64, 0x12, 0x10, 0x0a, 0x03, 0x6e, 0x75, 0x6d, 0x18, 0x02, + 0x20, 0x01, 0x28, 0x03, 0x52, 0x03, 0x6e, 0x75, 0x6d, 0x12, 0x1b, 0x0a, 0x08, 0x6c, 0x69, 0x6e, + 0x65, 0x5f, 0x6e, 0x75, 0x6d, 0x18, 0x03, 0x20, 0x01, 0x28, 0x03, 0x48, 0x00, 0x52, 0x07, 0x6c, + 0x69, 0x6e, 0x65, 0x4e, 0x75, 0x6d, 0x12, 0x14, 0x0a, 0x04, 0x6c, 0x61, 0x6e, 0x67, 0x18, 0x04, + 0x20, 0x01, 0x28, 0x09, 0x48, 0x00, 0x52, 0x04, 0x6c, 0x61, 0x6e, 0x67, 0x12, 0x2b, 0x0a, 0x06, + 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x18, 0x05, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x13, 0x2e, 0x74, + 0x65, 0x73, 0x74, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x45, 0x6d, 0x62, 0x65, 0x64, 0x64, 0x65, + 0x64, 0x52, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x12, 0x10, 0x0a, 0x02, 0x65, 0x6e, 0x18, + 0x06, 0x20, 0x01, 0x28, 0x03, 0x48, 0x01, 0x52, 0x02, 0x65, 0x6e, 0x12, 0x25, 0x0a, 0x02, 0x6e, + 0x6f, 0x18, 0x07, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x13, 0x2e, 0x74, 0x65, 0x73, 0x74, 0x70, 0x72, + 0x6f, 0x74, 0x6f, 0x2e, 0x45, 0x6d, 0x62, 0x65, 0x64, 0x64, 0x65, 0x64, 0x48, 0x01, 0x52, 0x02, + 0x6e, 0x6f, 0x12, 0x29, 0x0a, 0x06, 0x63, 0x6f, 0x72, 0x70, 0x75, 0x73, 0x18, 0x08, 0x20, 0x01, + 0x28, 0x0e, 0x32, 0x11, 0x2e, 0x74, 0x65, 0x73, 0x74, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x43, + 0x6f, 0x72, 0x70, 0x75, 0x73, 0x52, 0x06, 0x63, 0x6f, 0x72, 0x70, 0x75, 0x73, 0x42, 0x06, 0x0a, + 0x04, 0x63, 0x6f, 0x64, 0x65, 0x42, 0x05, 0x0a, 0x03, 0x65, 0x78, 0x74, 0x22, 0x85, 0x01, 0x0a, + 0x0e, 0x44, 0x79, 0x6e, 0x61, 0x6d, 0x69, 0x63, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x12, + 0x3a, 0x0a, 0x0c, 0x73, 0x74, 0x72, 0x75, 0x63, 0x74, 0x5f, 0x66, 0x69, 0x65, 0x6c, 0x64, 0x18, + 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x17, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, + 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x53, 0x74, 0x72, 0x75, 0x63, 0x74, 0x52, 0x0b, + 0x73, 0x74, 0x72, 0x75, 0x63, 0x74, 0x46, 0x69, 0x65, 0x6c, 0x64, 0x12, 0x37, 0x0a, 0x0b, 0x76, + 0x61, 0x6c, 0x75, 0x65, 0x5f, 0x66, 0x69, 0x65, 0x6c, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, + 0x32, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, + 0x75, 0x66, 0x2e, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x52, 0x0a, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x46, + 0x69, 0x65, 0x6c, 0x64, 0x22, 0x82, 0x01, 0x0a, 0x14, 0x44, 0x79, 0x6e, 0x61, 0x6d, 0x69, 0x63, + 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x12, 0x2d, 0x0a, + 0x04, 0x62, 0x6f, 0x64, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x19, 0x2e, 0x74, 0x65, + 0x73, 0x74, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x44, 0x79, 0x6e, 0x61, 0x6d, 0x69, 0x63, 0x4d, + 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x52, 0x04, 0x62, 0x6f, 0x64, 0x79, 0x12, 0x3b, 0x0a, 0x0b, + 0x75, 0x70, 0x64, 0x61, 0x74, 0x65, 0x5f, 0x6d, 0x61, 0x73, 0x6b, 0x18, 0x02, 0x20, 0x01, 0x28, + 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, + 0x62, 0x75, 0x66, 0x2e, 0x46, 0x69, 0x65, 0x6c, 0x64, 0x4d, 0x61, 0x73, 0x6b, 0x52, 0x0a, 0x75, + 0x70, 0x64, 0x61, 0x74, 0x65, 0x4d, 0x61, 0x73, 0x6b, 0x2a, 0x5a, 0x0a, 0x06, 0x43, 0x6f, 0x72, + 0x70, 0x75, 0x73, 0x12, 0x0d, 0x0a, 0x09, 0x55, 0x4e, 0x49, 0x56, 0x45, 0x52, 0x53, 0x41, 0x4c, + 0x10, 0x00, 0x12, 0x07, 0x0a, 0x03, 0x57, 0x45, 0x42, 0x10, 0x01, 0x12, 0x0a, 0x0a, 0x06, 0x49, + 0x4d, 0x41, 0x47, 0x45, 0x53, 0x10, 0x02, 0x12, 0x09, 0x0a, 0x05, 0x4c, 0x4f, 0x43, 0x41, 0x4c, + 0x10, 0x03, 0x12, 0x08, 0x0a, 0x04, 0x4e, 0x45, 0x57, 0x53, 0x10, 0x04, 0x12, 0x0c, 0x0a, 0x08, + 0x50, 0x52, 0x4f, 0x44, 0x55, 0x43, 0x54, 0x53, 0x10, 0x05, 0x12, 0x09, 0x0a, 0x05, 0x56, 0x49, + 0x44, 0x45, 0x4f, 0x10, 0x06, 0x32, 0xbc, 0x04, 0x0a, 0x0b, 0x45, 0x63, 0x68, 0x6f, 0x53, 0x65, + 0x72, 0x76, 0x69, 0x63, 0x65, 0x12, 0xf2, 0x01, 0x0a, 0x04, 0x45, 0x63, 0x68, 0x6f, 0x12, 0x18, + 0x2e, 0x74, 0x65, 0x73, 0x74, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x53, 0x69, 0x6d, 0x70, 0x6c, + 0x65, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x1a, 0x18, 0x2e, 0x74, 0x65, 0x73, 0x74, 0x70, + 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x53, 0x69, 0x6d, 0x70, 0x6c, 0x65, 0x4d, 0x65, 0x73, 0x73, 0x61, + 0x67, 0x65, 0x22, 0xb5, 0x01, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0xae, 0x01, 0x22, 0x15, 0x2f, 0x76, + 0x31, 0x2f, 0x65, 0x78, 0x61, 0x6d, 0x70, 0x6c, 0x65, 0x2f, 0x65, 0x63, 0x68, 0x6f, 0x2f, 0x7b, + 0x69, 0x64, 0x7d, 0x5a, 0x1d, 0x12, 0x1b, 0x2f, 0x76, 0x31, 0x2f, 0x65, 0x78, 0x61, 0x6d, 0x70, + 0x6c, 0x65, 0x2f, 0x65, 0x63, 0x68, 0x6f, 0x2f, 0x7b, 0x69, 0x64, 0x7d, 0x2f, 0x7b, 0x6e, 0x75, + 0x6d, 0x7d, 0x5a, 0x24, 0x12, 0x22, 0x2f, 0x76, 0x31, 0x2f, 0x65, 0x78, 0x61, 0x6d, 0x70, 0x6c, + 0x65, 0x2f, 0x65, 0x63, 0x68, 0x6f, 0x2f, 0x7b, 0x69, 0x64, 0x7d, 0x2f, 0x7b, 0x6e, 0x75, 0x6d, + 0x7d, 0x2f, 0x7b, 0x6c, 0x61, 0x6e, 0x67, 0x7d, 0x5a, 0x31, 0x12, 0x2f, 0x2f, 0x76, 0x31, 0x2f, + 0x65, 0x78, 0x61, 0x6d, 0x70, 0x6c, 0x65, 0x2f, 0x65, 0x63, 0x68, 0x6f, 0x31, 0x2f, 0x7b, 0x69, + 0x64, 0x7d, 0x2f, 0x7b, 0x6c, 0x69, 0x6e, 0x65, 0x5f, 0x6e, 0x75, 0x6d, 0x7d, 0x2f, 0x7b, 0x73, + 0x74, 0x61, 0x74, 0x75, 0x73, 0x2e, 0x6e, 0x6f, 0x74, 0x65, 0x7d, 0x5a, 0x1d, 0x12, 0x1b, 0x2f, + 0x76, 0x31, 0x2f, 0x65, 0x78, 0x61, 0x6d, 0x70, 0x6c, 0x65, 0x2f, 0x65, 0x63, 0x68, 0x6f, 0x32, + 0x2f, 0x7b, 0x6e, 0x6f, 0x2e, 0x6e, 0x6f, 0x74, 0x65, 0x7d, 0x12, 0x60, 0x0a, 0x08, 0x45, 0x63, + 0x68, 0x6f, 0x42, 0x6f, 0x64, 0x79, 0x12, 0x18, 0x2e, 0x74, 0x65, 0x73, 0x74, 0x70, 0x72, 0x6f, + 0x74, 0x6f, 0x2e, 0x53, 0x69, 0x6d, 0x70, 0x6c, 0x65, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, + 0x1a, 0x18, 0x2e, 0x74, 0x65, 0x73, 0x74, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x53, 0x69, 0x6d, + 0x70, 0x6c, 0x65, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x22, 0x20, 0x82, 0xd3, 0xe4, 0x93, + 0x02, 0x1a, 0x22, 0x15, 0x2f, 0x76, 0x31, 0x2f, 0x65, 0x78, 0x61, 0x6d, 0x70, 0x6c, 0x65, 0x2f, + 0x65, 0x63, 0x68, 0x6f, 0x5f, 0x62, 0x6f, 0x64, 0x79, 0x3a, 0x01, 0x2a, 0x12, 0x61, 0x0a, 0x0a, + 0x45, 0x63, 0x68, 0x6f, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x12, 0x18, 0x2e, 0x74, 0x65, 0x73, + 0x74, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x53, 0x69, 0x6d, 0x70, 0x6c, 0x65, 0x4d, 0x65, 0x73, + 0x73, 0x61, 0x67, 0x65, 0x1a, 0x18, 0x2e, 0x74, 0x65, 0x73, 0x74, 0x70, 0x72, 0x6f, 0x74, 0x6f, + 0x2e, 0x53, 0x69, 0x6d, 0x70, 0x6c, 0x65, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x22, 0x1f, + 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x19, 0x2a, 0x17, 0x2f, 0x76, 0x31, 0x2f, 0x65, 0x78, 0x61, 0x6d, + 0x70, 0x6c, 0x65, 0x2f, 0x65, 0x63, 0x68, 0x6f, 0x5f, 0x64, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x12, + 0x73, 0x0a, 0x09, 0x45, 0x63, 0x68, 0x6f, 0x50, 0x61, 0x74, 0x63, 0x68, 0x12, 0x1f, 0x2e, 0x74, + 0x65, 0x73, 0x74, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x44, 0x79, 0x6e, 0x61, 0x6d, 0x69, 0x63, + 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x1a, 0x1f, 0x2e, + 0x74, 0x65, 0x73, 0x74, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x44, 0x79, 0x6e, 0x61, 0x6d, 0x69, + 0x63, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x22, 0x24, + 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x1e, 0x32, 0x16, 0x2f, 0x76, 0x31, 0x2f, 0x65, 0x78, 0x61, 0x6d, + 0x70, 0x6c, 0x65, 0x2f, 0x65, 0x63, 0x68, 0x6f, 0x5f, 0x70, 0x61, 0x74, 0x63, 0x68, 0x3a, 0x04, + 0x62, 0x6f, 0x64, 0x79, 0x42, 0x51, 0x5a, 0x4f, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, + 0x6f, 0x6d, 0x2f, 0x67, 0x6f, 0x2d, 0x6b, 0x72, 0x61, 0x74, 0x6f, 0x73, 0x2f, 0x6b, 0x72, 0x61, + 0x74, 0x6f, 0x73, 0x2f, 0x63, 0x6d, 0x64, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x2d, 0x67, + 0x65, 0x6e, 0x2d, 0x67, 0x6f, 0x2d, 0x68, 0x74, 0x74, 0x70, 0x2f, 0x69, 0x6e, 0x74, 0x65, 0x72, + 0x6e, 0x61, 0x6c, 0x2f, 0x74, 0x65, 0x73, 0x74, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x3b, 0x74, 0x65, + 0x73, 0x74, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, +} + +var ( + file_echo_service_proto_rawDescOnce sync.Once + file_echo_service_proto_rawDescData = file_echo_service_proto_rawDesc +) + +func file_echo_service_proto_rawDescGZIP() []byte { + file_echo_service_proto_rawDescOnce.Do(func() { + file_echo_service_proto_rawDescData = protoimpl.X.CompressGZIP(file_echo_service_proto_rawDescData) + }) + return file_echo_service_proto_rawDescData +} + +var file_echo_service_proto_enumTypes = make([]protoimpl.EnumInfo, 1) +var file_echo_service_proto_msgTypes = make([]protoimpl.MessageInfo, 4) +var file_echo_service_proto_goTypes = []interface{}{ + (Corpus)(0), // 0: testproto.Corpus + (*Embedded)(nil), // 1: testproto.Embedded + (*SimpleMessage)(nil), // 2: testproto.SimpleMessage + (*DynamicMessage)(nil), // 3: testproto.DynamicMessage + (*DynamicMessageUpdate)(nil), // 4: testproto.DynamicMessageUpdate + (*_struct.Struct)(nil), // 5: google.protobuf.Struct + (*_struct.Value)(nil), // 6: google.protobuf.Value + (*field_mask.FieldMask)(nil), // 7: google.protobuf.FieldMask +} +var file_echo_service_proto_depIdxs = []int32{ + 1, // 0: testproto.SimpleMessage.status:type_name -> testproto.Embedded + 1, // 1: testproto.SimpleMessage.no:type_name -> testproto.Embedded + 0, // 2: testproto.SimpleMessage.corpus:type_name -> testproto.Corpus + 5, // 3: testproto.DynamicMessage.struct_field:type_name -> google.protobuf.Struct + 6, // 4: testproto.DynamicMessage.value_field:type_name -> google.protobuf.Value + 3, // 5: testproto.DynamicMessageUpdate.body:type_name -> testproto.DynamicMessage + 7, // 6: testproto.DynamicMessageUpdate.update_mask:type_name -> google.protobuf.FieldMask + 2, // 7: testproto.EchoService.Echo:input_type -> testproto.SimpleMessage + 2, // 8: testproto.EchoService.EchoBody:input_type -> testproto.SimpleMessage + 2, // 9: testproto.EchoService.EchoDelete:input_type -> testproto.SimpleMessage + 4, // 10: testproto.EchoService.EchoPatch:input_type -> testproto.DynamicMessageUpdate + 2, // 11: testproto.EchoService.Echo:output_type -> testproto.SimpleMessage + 2, // 12: testproto.EchoService.EchoBody:output_type -> testproto.SimpleMessage + 2, // 13: testproto.EchoService.EchoDelete:output_type -> testproto.SimpleMessage + 4, // 14: testproto.EchoService.EchoPatch:output_type -> testproto.DynamicMessageUpdate + 11, // [11:15] is the sub-list for method output_type + 7, // [7:11] is the sub-list for method input_type + 7, // [7:7] is the sub-list for extension type_name + 7, // [7:7] is the sub-list for extension extendee + 0, // [0:7] is the sub-list for field type_name +} + +func init() { file_echo_service_proto_init() } +func file_echo_service_proto_init() { + if File_echo_service_proto != nil { + return + } + if !protoimpl.UnsafeEnabled { + file_echo_service_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*Embedded); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_echo_service_proto_msgTypes[1].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*SimpleMessage); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_echo_service_proto_msgTypes[2].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*DynamicMessage); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_echo_service_proto_msgTypes[3].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*DynamicMessageUpdate); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + } + file_echo_service_proto_msgTypes[0].OneofWrappers = []interface{}{ + (*Embedded_Progress)(nil), + (*Embedded_Note)(nil), + } + file_echo_service_proto_msgTypes[1].OneofWrappers = []interface{}{ + (*SimpleMessage_LineNum)(nil), + (*SimpleMessage_Lang)(nil), + (*SimpleMessage_En)(nil), + (*SimpleMessage_No)(nil), + } + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: file_echo_service_proto_rawDesc, + NumEnums: 1, + NumMessages: 4, + NumExtensions: 0, + NumServices: 1, + }, + GoTypes: file_echo_service_proto_goTypes, + DependencyIndexes: file_echo_service_proto_depIdxs, + EnumInfos: file_echo_service_proto_enumTypes, + MessageInfos: file_echo_service_proto_msgTypes, + }.Build() + File_echo_service_proto = out.File + file_echo_service_proto_rawDesc = nil + file_echo_service_proto_goTypes = nil + file_echo_service_proto_depIdxs = nil +} diff --git a/cmd/protoc-gen-go-http/internal/testproto/echo_service.proto b/cmd/protoc-gen-go-http/internal/testproto/echo_service.proto new file mode 100644 index 000000000..adee8c6b3 --- /dev/null +++ b/cmd/protoc-gen-go-http/internal/testproto/echo_service.proto @@ -0,0 +1,101 @@ +syntax = "proto3"; + +option go_package = "github.com/go-kratos/kratos/cmd/protoc-gen-go-http/internal/testproto;testproto"; + +package testproto; + +import "google/api/annotations.proto"; +import "google/protobuf/field_mask.proto"; +import "google/protobuf/struct.proto"; + +enum Corpus { + UNIVERSAL = 0; + WEB = 1; + IMAGES = 2; + LOCAL = 3; + NEWS = 4; + PRODUCTS = 5; + VIDEO = 6; +} + +// Embedded represents a message embedded in SimpleMessage. +message Embedded { + oneof mark { + int64 progress = 1; + string note = 2; + } +} + +// SimpleMessage represents a simple message sent to the Echo service. +message SimpleMessage { + // Id represents the message identifier. + string id = 1; + int64 num = 2; + oneof code { + int64 line_num = 3; + string lang = 4; + } + Embedded status = 5; + oneof ext { + int64 en = 6; + Embedded no = 7; + } + Corpus corpus = 8; +} + +// DynamicMessage represents a message which can have its structure +// built dynamically using Struct and Values. +message DynamicMessage { + google.protobuf.Struct struct_field = 1; + google.protobuf.Value value_field = 2; +} + +message DynamicMessageUpdate { + DynamicMessage body = 1; + google.protobuf.FieldMask update_mask = 2; +} + +// Echo service responds to incoming echo requests. +service EchoService { + // Echo method receives a simple message and returns it. + // + // The message posted as the id parameter will also be + // returned. + rpc Echo(SimpleMessage) returns (SimpleMessage) { + option (google.api.http) = { + post: "/v1/example/echo/{id}" + additional_bindings { + get: "/v1/example/echo/{id}/{num}" + } + additional_bindings { + get: "/v1/example/echo/{id}/{num}/{lang}" + } + additional_bindings { + get: "/v1/example/echo1/{id}/{line_num}/{status.note}" + } + additional_bindings { + get: "/v1/example/echo2/{no.note}" + } + }; + } + // EchoBody method receives a simple message and returns it. + rpc EchoBody(SimpleMessage) returns (SimpleMessage) { + option (google.api.http) = { + post: "/v1/example/echo_body" + body: "*" + }; + } + // EchoDelete method receives a simple message and returns it. + rpc EchoDelete(SimpleMessage) returns (SimpleMessage) { + option (google.api.http) = { + delete: "/v1/example/echo_delete" + }; + } + // EchoPatch method receives a NonStandardUpdateRequest and returns it. + rpc EchoPatch(DynamicMessageUpdate) returns (DynamicMessageUpdate) { + option (google.api.http) = { + patch: "/v1/example/echo_patch" + body: "body" + }; + } +} diff --git a/cmd/protoc-gen-go-http/internal/testproto/echo_service_grpc.pb.go b/cmd/protoc-gen-go-http/internal/testproto/echo_service_grpc.pb.go new file mode 100644 index 000000000..51c231a60 --- /dev/null +++ b/cmd/protoc-gen-go-http/internal/testproto/echo_service_grpc.pb.go @@ -0,0 +1,219 @@ +// Code generated by protoc-gen-go-grpc. DO NOT EDIT. + +package testproto + +import ( + context "context" + grpc "google.golang.org/grpc" + codes "google.golang.org/grpc/codes" + status "google.golang.org/grpc/status" +) + +// This is a compile-time assertion to ensure that this generated file +// is compatible with the grpc package it is being compiled against. +const _ = grpc.SupportPackageIsVersion7 + +// EchoServiceClient is the client API for EchoService service. +// +// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream. +type EchoServiceClient interface { + // Echo method receives a simple message and returns it. + // + // The message posted as the id parameter will also be + // returned. + Echo(ctx context.Context, in *SimpleMessage, opts ...grpc.CallOption) (*SimpleMessage, error) + // EchoBody method receives a simple message and returns it. + EchoBody(ctx context.Context, in *SimpleMessage, opts ...grpc.CallOption) (*SimpleMessage, error) + // EchoDelete method receives a simple message and returns it. + EchoDelete(ctx context.Context, in *SimpleMessage, opts ...grpc.CallOption) (*SimpleMessage, error) + // EchoPatch method receives a NonStandardUpdateRequest and returns it. + EchoPatch(ctx context.Context, in *DynamicMessageUpdate, opts ...grpc.CallOption) (*DynamicMessageUpdate, error) +} + +type echoServiceClient struct { + cc grpc.ClientConnInterface +} + +func NewEchoServiceClient(cc grpc.ClientConnInterface) EchoServiceClient { + return &echoServiceClient{cc} +} + +func (c *echoServiceClient) Echo(ctx context.Context, in *SimpleMessage, opts ...grpc.CallOption) (*SimpleMessage, error) { + out := new(SimpleMessage) + err := c.cc.Invoke(ctx, "/testproto.EchoService/Echo", in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *echoServiceClient) EchoBody(ctx context.Context, in *SimpleMessage, opts ...grpc.CallOption) (*SimpleMessage, error) { + out := new(SimpleMessage) + err := c.cc.Invoke(ctx, "/testproto.EchoService/EchoBody", in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *echoServiceClient) EchoDelete(ctx context.Context, in *SimpleMessage, opts ...grpc.CallOption) (*SimpleMessage, error) { + out := new(SimpleMessage) + err := c.cc.Invoke(ctx, "/testproto.EchoService/EchoDelete", in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *echoServiceClient) EchoPatch(ctx context.Context, in *DynamicMessageUpdate, opts ...grpc.CallOption) (*DynamicMessageUpdate, error) { + out := new(DynamicMessageUpdate) + err := c.cc.Invoke(ctx, "/testproto.EchoService/EchoPatch", in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + +// EchoServiceServer is the server API for EchoService service. +// All implementations must embed UnimplementedEchoServiceServer +// for forward compatibility +type EchoServiceServer interface { + // Echo method receives a simple message and returns it. + // + // The message posted as the id parameter will also be + // returned. + Echo(context.Context, *SimpleMessage) (*SimpleMessage, error) + // EchoBody method receives a simple message and returns it. + EchoBody(context.Context, *SimpleMessage) (*SimpleMessage, error) + // EchoDelete method receives a simple message and returns it. + EchoDelete(context.Context, *SimpleMessage) (*SimpleMessage, error) + // EchoPatch method receives a NonStandardUpdateRequest and returns it. + EchoPatch(context.Context, *DynamicMessageUpdate) (*DynamicMessageUpdate, error) + mustEmbedUnimplementedEchoServiceServer() +} + +// UnimplementedEchoServiceServer must be embedded to have forward compatible implementations. +type UnimplementedEchoServiceServer struct { +} + +func (UnimplementedEchoServiceServer) Echo(context.Context, *SimpleMessage) (*SimpleMessage, error) { + return nil, status.Errorf(codes.Unimplemented, "method Echo not implemented") +} +func (UnimplementedEchoServiceServer) EchoBody(context.Context, *SimpleMessage) (*SimpleMessage, error) { + return nil, status.Errorf(codes.Unimplemented, "method EchoBody not implemented") +} +func (UnimplementedEchoServiceServer) EchoDelete(context.Context, *SimpleMessage) (*SimpleMessage, error) { + return nil, status.Errorf(codes.Unimplemented, "method EchoDelete not implemented") +} +func (UnimplementedEchoServiceServer) EchoPatch(context.Context, *DynamicMessageUpdate) (*DynamicMessageUpdate, error) { + return nil, status.Errorf(codes.Unimplemented, "method EchoPatch not implemented") +} +func (UnimplementedEchoServiceServer) mustEmbedUnimplementedEchoServiceServer() {} + +// UnsafeEchoServiceServer may be embedded to opt out of forward compatibility for this service. +// Use of this interface is not recommended, as added methods to EchoServiceServer will +// result in compilation errors. +type UnsafeEchoServiceServer interface { + mustEmbedUnimplementedEchoServiceServer() +} + +func RegisterEchoServiceServer(s grpc.ServiceRegistrar, srv EchoServiceServer) { + s.RegisterService(&_EchoService_serviceDesc, srv) +} + +func _EchoService_Echo_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(SimpleMessage) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(EchoServiceServer).Echo(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: "/testproto.EchoService/Echo", + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(EchoServiceServer).Echo(ctx, req.(*SimpleMessage)) + } + return interceptor(ctx, in, info, handler) +} + +func _EchoService_EchoBody_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(SimpleMessage) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(EchoServiceServer).EchoBody(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: "/testproto.EchoService/EchoBody", + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(EchoServiceServer).EchoBody(ctx, req.(*SimpleMessage)) + } + return interceptor(ctx, in, info, handler) +} + +func _EchoService_EchoDelete_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(SimpleMessage) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(EchoServiceServer).EchoDelete(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: "/testproto.EchoService/EchoDelete", + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(EchoServiceServer).EchoDelete(ctx, req.(*SimpleMessage)) + } + return interceptor(ctx, in, info, handler) +} + +func _EchoService_EchoPatch_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(DynamicMessageUpdate) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(EchoServiceServer).EchoPatch(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: "/testproto.EchoService/EchoPatch", + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(EchoServiceServer).EchoPatch(ctx, req.(*DynamicMessageUpdate)) + } + return interceptor(ctx, in, info, handler) +} + +var _EchoService_serviceDesc = grpc.ServiceDesc{ + ServiceName: "testproto.EchoService", + HandlerType: (*EchoServiceServer)(nil), + Methods: []grpc.MethodDesc{ + { + MethodName: "Echo", + Handler: _EchoService_Echo_Handler, + }, + { + MethodName: "EchoBody", + Handler: _EchoService_EchoBody_Handler, + }, + { + MethodName: "EchoDelete", + Handler: _EchoService_EchoDelete_Handler, + }, + { + MethodName: "EchoPatch", + Handler: _EchoService_EchoPatch_Handler, + }, + }, + Streams: []grpc.StreamDesc{}, + Metadata: "echo_service.proto", +} diff --git a/cmd/protoc-gen-go-http/internal/testproto/echo_service_http.pb.go b/cmd/protoc-gen-go-http/internal/testproto/echo_service_http.pb.go new file mode 100644 index 000000000..dfb5932f1 --- /dev/null +++ b/cmd/protoc-gen-go-http/internal/testproto/echo_service_http.pb.go @@ -0,0 +1,240 @@ +// Code generated by protoc-gen-go-http. DO NOT EDIT. + +package testproto + +import ( + context "context" + middleware "github.com/go-kratos/kratos/v2/middleware" + http1 "github.com/go-kratos/kratos/v2/transport/http" + http "net/http" +) + +// This is a compile-time assertion to ensure that this generated file +// is compatible with the kratos package it is being compiled against. +// context./http./middleware. +const _ = http1.SupportPackageIsVersion1 + +type EchoServiceHTTPServer interface { + Echo(context.Context, *SimpleMessage) (*SimpleMessage, error) + + EchoBody(context.Context, *SimpleMessage) (*SimpleMessage, error) + + EchoDelete(context.Context, *SimpleMessage) (*SimpleMessage, error) + + EchoPatch(context.Context, *DynamicMessageUpdate) (*DynamicMessageUpdate, error) +} + +func RegisterEchoServiceHTTPServer(s http1.ServiceRegistrar, srv EchoServiceHTTPServer) { + s.RegisterService(&_HTTP_EchoService_serviceDesc, srv) +} + +func _HTTP_EchoService_Echo_0(srv interface{}, ctx context.Context, req *http.Request, dec func(interface{}) error, m middleware.Middleware) (interface{}, error) { + var in SimpleMessage + + if err := http1.BindVars(req, &in); err != nil { + return nil, err + } + + if err := http1.BindForm(req, &in); err != nil { + return nil, err + } + + h := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(EchoServiceServer).Echo(ctx, &in) + } + out, err := m(h)(ctx, &in) + if err != nil { + return nil, err + } + return out, nil +} + +func _HTTP_EchoService_Echo_1(srv interface{}, ctx context.Context, req *http.Request, dec func(interface{}) error, m middleware.Middleware) (interface{}, error) { + var in SimpleMessage + + if err := http1.BindVars(req, &in); err != nil { + return nil, err + } + + if err := http1.BindForm(req, &in); err != nil { + return nil, err + } + + h := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(EchoServiceServer).Echo(ctx, &in) + } + out, err := m(h)(ctx, &in) + if err != nil { + return nil, err + } + return out, nil +} + +func _HTTP_EchoService_Echo_2(srv interface{}, ctx context.Context, req *http.Request, dec func(interface{}) error, m middleware.Middleware) (interface{}, error) { + var in SimpleMessage + + if err := http1.BindVars(req, &in); err != nil { + return nil, err + } + + if err := http1.BindForm(req, &in); err != nil { + return nil, err + } + + h := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(EchoServiceServer).Echo(ctx, &in) + } + out, err := m(h)(ctx, &in) + if err != nil { + return nil, err + } + return out, nil +} + +func _HTTP_EchoService_Echo_3(srv interface{}, ctx context.Context, req *http.Request, dec func(interface{}) error, m middleware.Middleware) (interface{}, error) { + var in SimpleMessage + + if err := http1.BindVars(req, &in); err != nil { + return nil, err + } + + if err := http1.BindForm(req, &in); err != nil { + return nil, err + } + + h := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(EchoServiceServer).Echo(ctx, &in) + } + out, err := m(h)(ctx, &in) + if err != nil { + return nil, err + } + return out, nil +} + +func _HTTP_EchoService_Echo_4(srv interface{}, ctx context.Context, req *http.Request, dec func(interface{}) error, m middleware.Middleware) (interface{}, error) { + var in SimpleMessage + + if err := http1.BindVars(req, &in); err != nil { + return nil, err + } + + if err := http1.BindForm(req, &in); err != nil { + return nil, err + } + + h := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(EchoServiceServer).Echo(ctx, &in) + } + out, err := m(h)(ctx, &in) + if err != nil { + return nil, err + } + return out, nil +} + +func _HTTP_EchoService_EchoBody_0(srv interface{}, ctx context.Context, req *http.Request, dec func(interface{}) error, m middleware.Middleware) (interface{}, error) { + var in SimpleMessage + + if err := dec(&in); err != nil { + return nil, err + } + + h := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(EchoServiceServer).EchoBody(ctx, &in) + } + out, err := m(h)(ctx, &in) + if err != nil { + return nil, err + } + return out, nil +} + +func _HTTP_EchoService_EchoDelete_0(srv interface{}, ctx context.Context, req *http.Request, dec func(interface{}) error, m middleware.Middleware) (interface{}, error) { + var in SimpleMessage + + if err := http1.BindForm(req, &in); err != nil { + return nil, err + } + + h := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(EchoServiceServer).EchoDelete(ctx, &in) + } + out, err := m(h)(ctx, &in) + if err != nil { + return nil, err + } + return out, nil +} + +func _HTTP_EchoService_EchoPatch_0(srv interface{}, ctx context.Context, req *http.Request, dec func(interface{}) error, m middleware.Middleware) (interface{}, error) { + var in DynamicMessageUpdate + + if err := dec(in.Body); err != nil { + return nil, err + } + + h := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(EchoServiceServer).EchoPatch(ctx, &in) + } + out, err := m(h)(ctx, &in) + if err != nil { + return nil, err + } + return out, nil +} + +var _HTTP_EchoService_serviceDesc = http1.ServiceDesc{ + ServiceName: "testproto.EchoService", + Methods: []http1.MethodDesc{ + + { + Path: "/v1/example/echo/{id}/{num}", + Method: "GET", + Handler: _HTTP_EchoService_Echo_0, + }, + + { + Path: "/v1/example/echo/{id}/{num}/{lang}", + Method: "GET", + Handler: _HTTP_EchoService_Echo_1, + }, + + { + Path: "/v1/example/echo1/{id}/{line_num}/{status.note}", + Method: "GET", + Handler: _HTTP_EchoService_Echo_2, + }, + + { + Path: "/v1/example/echo2/{no.note}", + Method: "GET", + Handler: _HTTP_EchoService_Echo_3, + }, + + { + Path: "/v1/example/echo/{id}", + Method: "POST", + Handler: _HTTP_EchoService_Echo_4, + }, + + { + Path: "/v1/example/echo_body", + Method: "POST", + Handler: _HTTP_EchoService_EchoBody_0, + }, + + { + Path: "/v1/example/echo_delete", + Method: "DELETE", + Handler: _HTTP_EchoService_EchoDelete_0, + }, + + { + Path: "/v1/example/echo_patch", + Method: "PATCH", + Handler: _HTTP_EchoService_EchoPatch_0, + }, + }, + Metadata: "echo_service.proto", +} diff --git a/cmd/protoc-gen-go-http/main.go b/cmd/protoc-gen-go-http/main.go new file mode 100644 index 000000000..86c436ef4 --- /dev/null +++ b/cmd/protoc-gen-go-http/main.go @@ -0,0 +1,35 @@ +package main + +import ( + "flag" + "fmt" + + "google.golang.org/protobuf/compiler/protogen" + "google.golang.org/protobuf/types/pluginpb" +) + +const version = "0.0.1" + +func main() { + showVersion := flag.Bool("version", false, "print the version and exit") + flag.Parse() + if *showVersion { + fmt.Printf("protoc-gen-go-http %v\n", version) + return + } + + var flags flag.FlagSet + + protogen.Options{ + ParamFunc: flags.Set, + }.Run(func(gen *protogen.Plugin) error { + gen.SupportedFeatures = uint64(pluginpb.CodeGeneratorResponse_FEATURE_PROTO3_OPTIONAL) + for _, f := range gen.Files { + if !f.Generate { + continue + } + generateFile(gen, f) + } + return nil + }) +} diff --git a/cmd/protoc-gen-go-http/template.go b/cmd/protoc-gen-go-http/template.go new file mode 100644 index 000000000..3537df4f9 --- /dev/null +++ b/cmd/protoc-gen-go-http/template.go @@ -0,0 +1,101 @@ +package main + +import ( + "bytes" + "html/template" + "strings" +) + +var httpTemplate = ` +type {{.ServiceType}}HTTPServer interface { +{{range .MethodSets}} + {{.Name}}(context.Context, *{{.Request}}) (*{{.Reply}}, error) +{{end}} +} +func Register{{.ServiceType}}HTTPServer(s http1.ServiceRegistrar, srv {{.ServiceType}}HTTPServer) { + s.RegisterService(&_HTTP_{{.ServiceType}}_serviceDesc, srv) +} +{{range .Methods}} +func _HTTP_{{$.ServiceType}}_{{.Name}}_{{.Num}}(srv interface{}, ctx context.Context, req *http.Request, dec func(interface{}) error, m middleware.Middleware) (interface{}, error) { + var in {{.Request}} +{{if ne (len .Vars) 0}} + if err := http1.BindVars(req, &in); err != nil { + return nil, err + } +{{end}} +{{if eq .Body ""}} + if err := http1.BindForm(req, &in); err != nil { + return nil, err + } +{{else if eq .Body ".*"}} + if err := dec(&in); err != nil { + return nil, err + } +{{else}} + if err := dec(in{{.Body}}); err != nil { + return nil, err + } +{{end}} + h := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.({{$.ServiceType}}Server).{{.Name}}(ctx, &in) + } + out, err := m(h)(ctx, &in) + if err != nil { + return nil, err + } + return out{{.ResponseBody}}, nil +} +{{end}} +var _HTTP_{{.ServiceType}}_serviceDesc = http1.ServiceDesc{ + ServiceName: "{{.ServiceName}}", + Methods: []http1.MethodDesc{ +{{range .Methods}} + { + Path: "{{.Path}}", + Method: "{{.Method}}", + Handler: _HTTP_{{$.ServiceType}}_{{.Name}}_{{.Num}}, + }, +{{end}} + }, + Metadata: "{{.Metadata}}", +} +` + +type serviceDesc struct { + ServiceType string // Greeter + ServiceName string // helloworld.Greeter + Metadata string // api/helloworld/helloworld.proto + Methods []*methodDesc + MethodSets map[string]*methodDesc +} + +type methodDesc struct { + // method + Name string + Num int + Vars []string + Forms []string + Request string + Reply string + // http_rule + Path string + Method string + Body string + ResponseBody string +} + +func (s *serviceDesc) execute() string { + s.MethodSets = make(map[string]*methodDesc) + for _, m := range s.Methods { + s.MethodSets[m.Name] = m + } + buf := new(bytes.Buffer) + tmpl, err := template.New("http").Parse(strings.TrimSpace(httpTemplate)) + if err != nil { + panic(err) + } + if err := tmpl.Execute(buf, s); err != nil { + panic(err) + } + return string(buf.Bytes()) +} diff --git a/config/README.md b/config/README.md new file mode 100644 index 000000000..ea01387d0 --- /dev/null +++ b/config/README.md @@ -0,0 +1,37 @@ +# config + +可以指定多个配置源,config 会进行合并成 map[string]interface{},然后通过 Scan 或者 Value 获取值内容; + +``` +c := config.New( + config.WithSource( + file.NewSource(path), + ), + config.WithDecoder(func(kv *config.KeyValue, v map[string]interface{}) error { + // kv.Key + // kv.Value + // kv.Metadata + // 自定义实现对应的数据源解析,如果是配置中心数据源也可以指定metadata进行识别配置类型 + return yaml.Unmarshal(kv.Value, v) + }), +) +// 加载配置源: +if err := c.Load(); err != nil { + panic(err) +} +// 获取对应的值内容: +name, err := c.Value("service").String() +// 解析到结构体(由于已经合并到map[string]interface{},所以需要指定 jsonName 进行解析): +var v struct { + Service string `json:"service"` + Version string `json:"version"` +} +if err := c.Scan(&v); err != nil { + panic(err) +} +// 监听值内容变更 +c.Watch("service.name", func(key string, value config.Value) { + // 值内容变更 +}) +``` + diff --git a/config/config.go b/config/config.go new file mode 100644 index 000000000..15ef8e678 --- /dev/null +++ b/config/config.go @@ -0,0 +1,141 @@ +package config + +import ( + "encoding/json" + "errors" + "reflect" + "sync" + "time" + + "github.com/go-kratos/kratos/v2/log" +) + +var ( + // ErrNotFound is key not found. + ErrNotFound = errors.New("key not found") + // ErrTypeAssert is type assert error. + ErrTypeAssert = errors.New("type assert error") + + _ Config = (*config)(nil) +) + +// Observer is config observer. +type Observer func(string, Value) + +// Config is a config interface. +type Config interface { + Load() error + Scan(v interface{}) error + Value(key string) Value + Watch(key string, o Observer) error + Close() error +} + +type config struct { + opts options + reader Reader + cached sync.Map + observers sync.Map + watchers []Watcher + log *log.Helper +} + +// New new a config with options. +func New(opts ...Option) Config { + options := options{ + logger: log.DefaultLogger, + decoder: func(kv *KeyValue, v map[string]interface{}) error { + return json.Unmarshal(kv.Value, &v) + }, + } + for _, o := range opts { + o(&options) + } + return &config{ + opts: options, + reader: newReader(options), + log: log.NewHelper("config", options.logger), + } +} + +func (c *config) watch(w Watcher) { + for { + kvs, err := w.Next() + if err != nil { + time.Sleep(time.Second) + c.log.Errorf("Failed to watch next config: %v", err) + continue + } + if err := c.reader.Merge(kvs...); err != nil { + c.log.Errorf("Failed to merge next config: %v", err) + continue + } + c.cached.Range(func(key, value interface{}) bool { + k := key.(string) + v := value.(Value) + if n, ok := c.reader.Value(k); ok && !reflect.DeepEqual(n.Load(), v.Load()) { + v.Store(n.Load()) + if o, ok := c.observers.Load(k); ok { + o.(Observer)(k, v) + } + } + return true + }) + } +} + +func (c *config) Load() error { + for _, src := range c.opts.sources { + kvs, err := src.Load() + if err != nil { + return err + } + if err := c.reader.Merge(kvs...); err != nil { + c.log.Errorf("Failed to merge config source: %v", err) + return err + } + w, err := src.Watch() + if err != nil { + c.log.Errorf("Failed to watch config source: %v", err) + return err + } + go c.watch(w) + } + return nil +} + +func (c *config) Value(key string) Value { + if v, ok := c.cached.Load(key); ok { + return v.(Value) + } + if v, ok := c.reader.Value(key); ok { + c.cached.Store(key, v) + return v + } + return &errValue{err: ErrNotFound} +} + +func (c *config) Scan(v interface{}) error { + data, err := c.reader.Source() + if err != nil { + return err + } + return json.Unmarshal(data, v) +} + +func (c *config) Watch(key string, o Observer) error { + if v := c.Value(key); v.Load() == nil { + return ErrNotFound + } + c.observers.Store(key, o) + return nil +} + +func (c *config) Close() error { + for _, w := range c.watchers { + if err := w.Close(); err != nil { + return err + } + } + return nil +} diff --git a/config/file/file.go b/config/file/file.go new file mode 100644 index 000000000..5959666f0 --- /dev/null +++ b/config/file/file.go @@ -0,0 +1,79 @@ +package file + +import ( + "io/ioutil" + "os" + "path/filepath" + "strings" + + "github.com/go-kratos/kratos/v2/config" +) + +var _ config.Source = (*file)(nil) + +type file struct { + path string +} + +// NewSource new a file source. +func NewSource(path string) config.Source { + return &file{path: path} +} + +func (f *file) loadFile(path string) (*config.KeyValue, error) { + file, err := os.Open(path) + if err != nil { + return nil, err + } + defer file.Close() + data, err := ioutil.ReadAll(file) + if err != nil { + return nil, err + } + info, err := file.Stat() + if err != nil { + return nil, err + } + return &config.KeyValue{ + Key: info.Name(), + Value: data, + }, nil +} + +func (f *file) loadDir(path string) (kvs []*config.KeyValue, err error) { + files, err := ioutil.ReadDir(f.path) + if err != nil { + return nil, err + } + for _, file := range files { + // ignore hidden files + if file.IsDir() || strings.HasPrefix(file.Name(), ".") { + continue + } + kv, err := f.loadFile(filepath.Join(f.path, file.Name())) + if err != nil { + return nil, err + } + kvs = append(kvs, kv) + } + return +} + +func (f *file) Load() (kvs []*config.KeyValue, err error) { + fi, err := os.Stat(f.path) + if err != nil { + return nil, err + } + if fi.IsDir() { + return f.loadDir(f.path) + } + kv, err := f.loadFile(f.path) + if err != nil { + return nil, err + } + return []*config.KeyValue{kv}, nil +} + +func (f *file) Watch() (config.Watcher, error) { + return newWatcher(f) +} diff --git a/config/file/file_test.go b/config/file/file_test.go new file mode 100644 index 000000000..8bfe465b7 --- /dev/null +++ b/config/file/file_test.go @@ -0,0 +1,142 @@ +package file + +import ( + "errors" + "io/ioutil" + "os" + "path/filepath" + "testing" + "time" + + "github.com/go-kratos/kratos/v2/config" +) + +const ( + _testJSON = ` +{ + "test": { + "settings" : { + "int_key": 1000, + "float_key": 1000.1, + "duration_key": 10000, + "string_key": "string_value" + }, + "server": { + "addr": "127.0.0.1", + "port": 8000 + } + } +}` +) + +func TestFile(t *testing.T) { + var ( + path = filepath.Join(os.TempDir(), "test_config") + file = filepath.Join(path, "test.json") + data = []byte(_testJSON) + ) + defer os.Remove(path) + if err := os.MkdirAll(path, 0700); err != nil { + t.Error(err) + } + if err := ioutil.WriteFile(file, data, 0666); err != nil { + t.Error(err) + } + testSource(t, file, data) + testSource(t, path, data) +} + +func testSource(t *testing.T, path string, data []byte) { + t.Log(path) + + s := NewSource(path) + kvs, err := s.Load() + if err != nil { + t.Error(err) + } + if string(kvs[0].Value) != string(data) { + t.Errorf("no expected: %s, but got: %s", kvs[0].Value, data) + } +} + +func TestConfig(t *testing.T) { + path := filepath.Join(os.TempDir(), "test_config.json") + defer os.Remove(path) + if err := ioutil.WriteFile(path, []byte(_testJSON), 0666); err != nil { + t.Error(err) + } + c := config.New(config.WithSource( + NewSource(path), + )) + testConfig(t, c) +} + +func testConfig(t *testing.T, c config.Config) { + var expected = map[string]interface{}{ + "test.settings.int_key": int64(1000), + "test.settings.float_key": float64(1000.1), + "test.settings.string_key": "string_value", + "test.settings.duration_key": time.Duration(10000), + "test.server.addr": "127.0.0.1", + "test.server.port": int64(8000), + } + if err := c.Load(); err != nil { + t.Error(err) + } + for key, value := range expected { + switch value.(type) { + case int64: + if v, err := c.Value(key).Int(); err != nil { + t.Error(key, value, err) + } else if v != value { + t.Errorf("no expect key: %s value: %v, but got: %v", key, value, v) + } + case float64: + if v, err := c.Value(key).Float(); err != nil { + t.Error(key, value, err) + } else if v != value { + t.Errorf("no expect key: %s value: %v, but got: %v", key, value, v) + } + case string: + if v, err := c.Value(key).String(); err != nil { + t.Error(key, value, err) + } else if v != value { + t.Errorf("no expect key: %s value: %v, but got: %v", key, value, v) + } + case time.Duration: + if v, err := c.Value(key).Duration(); err != nil { + t.Error(key, value, err) + } else if v != value { + t.Errorf("no expect key: %s value: %v, but got: %v", key, value, v) + } + } + } + // scan + var settings struct { + IntKey int64 `json:"int_key"` + FloatKey float64 `json:"float_key"` + StringKey string `json:"string_key"` + DurationKey time.Duration `json:"duration_key"` + } + if err := c.Value("test.settings").Scan(&settings); err != nil { + t.Error(err) + } + if v := expected["test.settings.int_key"]; settings.IntKey != v { + t.Errorf("no expect int_key value: %v, but got: %v", settings.IntKey, v) + } + if v := expected["test.settings.float_key"]; settings.FloatKey != v { + t.Errorf("no expect float_key value: %v, but got: %v", settings.FloatKey, v) + } + if v := expected["test.settings.string_key"]; settings.StringKey != v { + t.Errorf("no expect string_key value: %v, but got: %v", settings.StringKey, v) + } + if v := expected["test.settings.duration_key"]; settings.DurationKey != v { + t.Errorf("no expect duration_key value: %v, but got: %v", settings.DurationKey, v) + } + + // not found + if _, err := c.Value("not_found_key").Bool(); errors.Is(err, config.ErrNotFound) { + t.Logf("not_found_key not match: %v", err) + } + +} diff --git a/config/file/watcher.go b/config/file/watcher.go new file mode 100644 index 000000000..7b53351e3 --- /dev/null +++ b/config/file/watcher.go @@ -0,0 +1,53 @@ +package file + +import ( + "os" + "path/filepath" + + "github.com/fsnotify/fsnotify" + "github.com/go-kratos/kratos/v2/config" +) + +type watcher struct { + f *file + fw *fsnotify.Watcher +} + +func newWatcher(f *file) (config.Watcher, error) { + fw, err := fsnotify.NewWatcher() + if err != nil { + return nil, err + } + fw.Add(f.path) + return &watcher{f: f, fw: fw}, nil +} + +func (w *watcher) Next() ([]*config.KeyValue, error) { + select { + case event := <-w.fw.Events: + if event.Op == fsnotify.Rename { + if _, err := os.Stat(event.Name); err == nil || os.IsExist(err) { + w.fw.Add(event.Name) + } + } + fi, err := os.Stat(w.f.path) + if err != nil { + return nil, err + } + path := w.f.path + if fi.IsDir() { + path = filepath.Join(w.f.path, event.Name) + } + kv, err := w.f.loadFile(path) + if err != nil { + return nil, err + } + return []*config.KeyValue{kv}, nil + case err := <-w.fw.Errors: + return nil, err + } +} + +func (w *watcher) Close() error { + return w.fw.Close() +} diff --git a/config/options.go b/config/options.go new file mode 100644 index 000000000..ec583e3fe --- /dev/null +++ b/config/options.go @@ -0,0 +1,38 @@ +package config + +import ( + "github.com/go-kratos/kratos/v2/log" +) + +// Decoder is config decoder. +type Decoder func(*KeyValue, map[string]interface{}) error + +// Option is config option. +type Option func(*options) + +type options struct { + sources []Source + decoder Decoder + logger log.Logger +} + +// WithSource with config source. +func WithSource(s ...Source) Option { + return func(o *options) { + o.sources = s + } +} + +// WithDecoder with config decoder. +func WithDecoder(d Decoder) Option { + return func(o *options) { + o.decoder = d + } +} + +// WithLogger with config loogger. +func WithLogger(l log.Logger) Option { + return func(o *options) { + o.logger = l + } +} diff --git a/config/reader.go b/config/reader.go new file mode 100644 index 000000000..de6c24bc5 --- /dev/null +++ b/config/reader.go @@ -0,0 +1,107 @@ +package config + +import ( + "encoding/json" + "fmt" + "strings" + + "github.com/imdario/mergo" +) + +// Reader is config reader. +type Reader interface { + Merge(...*KeyValue) error + Value(string) (Value, bool) + Source() ([]byte, error) +} + +type reader struct { + opts options + values map[string]interface{} +} + +func newReader(opts options) Reader { + return &reader{ + opts: opts, + values: make(map[string]interface{}), + } +} + +func (r *reader) Merge(kvs ...*KeyValue) error { + merged, err := cloneMap(r.values) + if err != nil { + return err + } + for _, kv := range kvs { + next := make(map[string]interface{}) + if err := r.opts.decoder(kv, next); err != nil { + return err + } + if err := mergo.Map(&merged, convertMap(next), mergo.WithOverride); err != nil { + return err + } + } + r.values = merged + return nil +} + +func (r *reader) Value(path string) (Value, bool) { + var ( + next = r.values + keys = strings.Split(path, ".") + last = len(keys) - 1 + ) + for idx, key := range keys { + value, ok := next[key] + if !ok { + return nil, false + } + if idx == last { + av := &atomicValue{} + av.Store(value) + return av, true + } + switch vm := value.(type) { + case map[string]interface{}: + next = vm + default: + return nil, false + } + } + return nil, false +} + +func (r *reader) Source() ([]byte, error) { + return json.Marshal(r.values) +} + +func cloneMap(src map[string]interface{}) (map[string]interface{}, error) { + data, err := json.Marshal(src) + if err != nil { + return nil, err + } + dst := make(map[string]interface{}) + if err = json.Unmarshal(data, &dst); err != nil { + return nil, err + } + return dst, nil +} + +func convertMap(src interface{}) interface{} { + switch m := src.(type) { + case map[string]interface{}: + dst := make(map[string]interface{}, len(m)) + for k, v := range m { + dst[k] = convertMap(v) + } + return dst + case map[interface{}]interface{}: + dst := make(map[string]interface{}, len(m)) + for k, v := range m { + dst[fmt.Sprint(k)] = convertMap(v) + } + return dst + default: + return src + } +} diff --git a/config/source.go b/config/source.go new file mode 100644 index 000000000..2c15ce8cd --- /dev/null +++ b/config/source.go @@ -0,0 +1,20 @@ +package config + +// KeyValue is config key value. +type KeyValue struct { + Key string + Value []byte + Metadata map[string]string +} + +// Source is config source. +type Source interface { + Load() ([]*KeyValue, error) + Watch() (Watcher, error) +} + +// Watcher watches a source for changes. +type Watcher interface { + Next() ([]*KeyValue, error) + Close() error +} diff --git a/config/value.go b/config/value.go new file mode 100644 index 000000000..52ed86b4b --- /dev/null +++ b/config/value.go @@ -0,0 +1,105 @@ +package config + +import ( + "encoding/json" + "fmt" + "reflect" + "strconv" + "sync/atomic" + "time" + + "google.golang.org/protobuf/encoding/protojson" + "google.golang.org/protobuf/proto" +) + +var ( + _ Value = (*atomicValue)(nil) + _ Value = (*errValue)(nil) +) + +// Value is config value interface. +type Value interface { + Bool() (bool, error) + Int() (int64, error) + Float() (float64, error) + String() (string, error) + Duration() (time.Duration, error) + Scan(interface{}) error + Load() interface{} + Store(interface{}) +} + +type atomicValue struct { + atomic.Value +} + +func (v *atomicValue) Bool() (bool, error) { + switch val := v.Load().(type) { + case bool: + return val, nil + case int64, float64, string: + return strconv.ParseBool(fmt.Sprint(val)) + } + return false, fmt.Errorf("type assert to %v failed", reflect.TypeOf(v.Load())) +} +func (v *atomicValue) Int() (int64, error) { + switch val := v.Load().(type) { + case int64: + return int64(val), nil + case float64: + return int64(val), nil + case string: + return strconv.ParseInt(val, 10, 64) + } + return 0, fmt.Errorf("type assert to %v failed", reflect.TypeOf(v.Load())) +} +func (v *atomicValue) Float() (float64, error) { + switch val := v.Load().(type) { + case float64: + return float64(val), nil + case int64: + return float64(val), nil + case string: + return strconv.ParseFloat(val, 10) + } + return 0.0, fmt.Errorf("type assert to %v failed", reflect.TypeOf(v.Load())) +} +func (v *atomicValue) String() (string, error) { + switch val := v.Load().(type) { + case string: + return val, nil + case bool, int64, float64: + return fmt.Sprint(val), nil + } + return "", fmt.Errorf("type assert to %v failed", reflect.TypeOf(v.Load())) +} +func (v *atomicValue) Duration() (time.Duration, error) { + val, err := v.Int() + if err != nil { + return 0, err + } + return time.Duration(val), nil +} +func (v *atomicValue) Scan(obj interface{}) error { + data, err := json.Marshal(v.Load()) + if err != nil { + return err + } + if pb, ok := obj.(proto.Message); ok { + return protojson.Unmarshal(data, pb) + } + return json.Unmarshal(data, obj) +} + +type errValue struct { + err error +} + +func (v errValue) Bool() (bool, error) { return false, v.err } +func (v errValue) Int() (int64, error) { return 0, v.err } +func (v errValue) Float() (float64, error) { return 0.0, v.err } +func (v errValue) Duration() (time.Duration, error) { return 0, v.err } +func (v errValue) String() (string, error) { return "", v.err } +func (v errValue) Scan(interface{}) error { return v.err } +func (v errValue) Load() interface{} { return nil } +func (v errValue) Store(interface{}) {} diff --git a/docs/.nojekyll b/docs/.nojekyll deleted file mode 100644 index e69de29bb..000000000 diff --git a/docs/CNAME b/docs/CNAME deleted file mode 100644 index 203bf8deb..000000000 --- a/docs/CNAME +++ /dev/null @@ -1 +0,0 @@ -v1.go-kratos.dev \ No newline at end of file diff --git a/docs/FAQ.md b/docs/FAQ.md deleted file mode 100644 index 768e19318..000000000 --- a/docs/FAQ.md +++ /dev/null @@ -1,21 +0,0 @@ -# 安装失败,提示go mod 错误 - -执行 -```shell -go get -u github.com/go-kratos/kratos/tool/kratos -``` -出现以下错误时 -```shell -go: github.com/prometheus/client_model@v0.0.0-20190220174349-fd36f4220a90: parsing go.mod: missing module line -go: github.com/remyoudompheng/bigfft@v0.0.0-20190806203942-babf20351dd7e3ac320adedbbe5eb311aec8763c: parsing go.mod: missing module line -``` -如果你使用了https://goproxy.io/ 代理,那你要使用其他代理来替换它,然后删除GOPATH目录下的mod缓存文件夹(`go clean --modcache`),然后重新执行安装命令 - -代理列表 - -``` -export GOPROXY=https://mirrors.aliyun.com/goproxy/ -export GOPROXY=https://goproxy.cn/ -export GOPROXY=https://goproxy.io/ -``` - diff --git a/docs/README.md b/docs/README.md index 13c1d0a72..e69de29bb 100644 --- a/docs/README.md +++ b/docs/README.md @@ -1,36 +0,0 @@ -![kratos](img/kratos3.png) -# Kratos - -Kratos是bilibili开源的一套Go微服务框架,包含大量微服务相关框架及工具。 - -### Goals - -我们致力于提供完整的微服务研发体验,整合相关框架及工具后,微服务治理相关部分可对整体业务开发周期无感,从而更加聚焦于业务交付。对每位开发者而言,整套Kratos框架也是不错的学习仓库,可以了解和参考到bilibili在微服务方面的技术积累和经验。 - -### Principles - -* 简单:不过度设计,代码平实简单 -* 通用:通用业务开发所需要的基础库的功能 -* 高效:提高业务迭代的效率 -* 稳定:基础库可测试性高,覆盖率高,有线上实践安全可靠 -* 健壮:通过良好的基础库设计,减少错用 -* 高性能:性能高,但不特定为了性能做hack优化,引入unsafe -* 扩展性:良好的接口设计,来扩展实现,或者通过新增基础库目录来扩展功能 -* 容错性:为失败设计,大量引入对SRE的理解,鲁棒性高 -* 工具链:包含大量工具链,比如cache代码生成,lint工具等等 - -### Features -* HTTP Blademaster:核心基于[gin](https://github.com/gin-gonic/gin)进行模块化设计,简单易用、核心足够轻量; -* GRPC Warden:基于官方gRPC开发,集成[discovery](https://github.com/bilibili/discovery)服务发现,并融合P2C负载均衡; -* Cache:优雅的接口化设计,非常方便的缓存序列化,推荐结合代理模式[overlord](https://github.com/bilibili/overlord); -* Database:集成MySQL/HBase/TiDB,添加熔断保护和统计支持,可快速发现数据层压力; -* Config:方便易用的[paladin sdk](config-paladin.md),可配合远程配置中心,实现配置版本管理和更新; -* Log:类似[zap](https://github.com/uber-go/zap)的field实现高性能日志库,并结合log-agent实现远程日志管理; -* Trace:基于opentracing,集成了全链路trace支持(gRPC/HTTP/MySQL/Redis/Memcached); -* Kratos Tool:工具链,可快速生成标准项目,或者通过Protobuf生成代码,非常便捷使用gRPC、HTTP、swagger文档; - - -------------- - -> 名字来源于:《战神》游戏以希腊神话为背景,讲述由凡人成为战神的奎托斯(Kratos)成为战神并展开弑神屠杀的冒险历程。 - diff --git a/docs/_sidebar.md b/docs/_sidebar.md deleted file mode 100644 index 84477b09d..000000000 --- a/docs/_sidebar.md +++ /dev/null @@ -1,39 +0,0 @@ -* [介绍](README.md) - * [快速开始 - 项目初始化](quickstart.md) -* [FAQ](FAQ.md) -* [http blademaster](blademaster.md) - * [bm quickstart](blademaster-quickstart.md) - * [bm module](blademaster-mod.md) - * [bm middleware](blademaster-mid.md) - * [bm protobuf](blademaster-pb.md) -* [grpc warden](warden.md) - * [warden quickstart](warden-quickstart.md) - * [warden interceptor](warden-mid.md) - * [warden resolver](warden-resolver.md) - * [warden balancer](warden-balancer.md) - * [warden protobuf](warden-pb.md) -* [config](config.md) - * [paladin](config-paladin.md) -* [ecode](ecode.md) -* [trace](trace.md) -* [log](logger.md) - * [log-agent](log-agent.md) -* [database](database.md) - * [mysql](database-mysql.md) - * [mysql-orm](database-mysql-orm.md) - * [hbase](database-hbase.md) - * [tidb](database-tidb.md) -* [cache](cache.md) - * [memcache](cache-mc.md) - * [redis](cache-redis.md) -* [kratos工具](kratos-tool.md) - * [protoc](kratos-protoc.md) - * [swagger](kratos-swagger.md) - * [genmc](kratos-genmc.md) - * [genbts](kratos-genbts.md) -* [限流bbr](ratelimit.md) -* [熔断breaker](breaker.md) -* [UT单元测试](ut.md) - * [testcli UT运行环境构建工具](ut-testcli.md) - * [testgen UT代码自动生成器](ut-testgen.md) - * [support UT周边辅助工具](ut-support.md) \ No newline at end of file diff --git a/docs/blademaster-mid.md b/docs/blademaster-mid.md deleted file mode 100644 index e379b97dc..000000000 --- a/docs/blademaster-mid.md +++ /dev/null @@ -1,177 +0,0 @@ -# 背景 - -基于bm的handler机制,可以自定义很多middleware(中间件)进行通用的业务处理,比如用户登录鉴权。接下来就以鉴权为例,说明middleware的写法和用法。 - -# 写自己的中间件 - -middleware本质上就是一个handler,接口和方法声明如下代码: - -```go -// Handler responds to an HTTP request. -type Handler interface { - ServeHTTP(c *Context) -} - -// HandlerFunc http request handler function. -type HandlerFunc func(*Context) - -// ServeHTTP calls f(ctx). -func (f HandlerFunc) ServeHTTP(c *Context) { - f(c) -} -``` - -1. 实现了`Handler`接口,可以作为engine的全局中间件使用:`engine.Use(YourHandler)` -2. 声明为`HandlerFunc`方法,可以作为engine的全局中间件使用:`engine.UseFunc(YourHandlerFunc)`,也可以作为router的局部中间件使用:`e.GET("/path", YourHandlerFunc)` - -简单示例代码如下: - -```go -type Demo struct { - Key string - Value string -} -// ServeHTTP implements from Handler interface -func (d *Demo) ServeHTTP(ctx *bm.Context) { - ctx.Set(d.Key, d.Value) -} - -e := bm.DefaultServer(nil) -d := &Demo{} - -// Handler使用如下: -e.Use(d) - -// HandlerFunc使用如下: -e.UseFunc(d.ServeHTTP) -e.GET("/path", d.ServeHTTP) - -// 或者只有方法 -myHandler := func(ctx *bm.Context) { - // some code -} -e.UseFunc(myHandler) -e.GET("/path", myHandler) -``` - -# 全局中间件 - -在blademaster的`server.go`代码中,有以下代码: - -```go -func DefaultServer(conf *ServerConfig) *Engine { - engine := NewServer(conf) - engine.Use(Recovery(), Trace(), Logger()) - return engine -} -``` - -会默认创建一个`bm engine`,并注册`Recovery(), Trace(), Logger()`三个middlerware用于全局handler处理,优先级从前到后。如果想要将自定义的middleware注册进全局,可以继续调用Use方法如下: - -```go -engine.Use(YourMiddleware()) -``` - -此方法会将`YourMiddleware`追加到已有的全局middleware后执行。如果需要全部自定义全局执行的middleware,可以使用`NewServer`方法创建一个无middleware的engine对象,然后使用`engine.Use/UseFunc`进行注册。 - -# 局部中间件 - -先来看一段鉴权伪代码示例([auth示例代码位置](https://github.com/go-kratos/kratos/tree/master/example/blademaster/middleware/auth)): - -```go -func Example() { - myHandler := func(ctx *bm.Context) { - mid := metadata.Int64(ctx, metadata.Mid) - ctx.JSON(fmt.Sprintf("%d", mid), nil) - } - - authn := auth.New(&auth.Config{DisableCSRF: false}) - - e := bm.DefaultServer(nil) - - // "/user"接口必须保证登录用户才能访问,那么我们加入"auth.User"来确保用户鉴权通过,才能进入myHandler进行业务逻辑处理 - e.GET("/user", authn.User, myHandler) - // "/guest"接口访客用户就可以访问,但如果登录用户我们需要知道mid,那么我们加入"auth.Guest"来尝试鉴权获取mid,但肯定会继续执行myHandler进行业务逻辑处理 - e.GET("/guest", authn.Guest, myHandler) - - // "/owner"开头的所有接口,都需要进行登录鉴权才可以被访问,那可以创建一个group并加入"authn.User" - o := e.Group("/owner", authn.User) - o.GET("/info", myHandler) // 该group创建的router不需要再显示的加入"authn.User" - o.POST("/modify", myHandler) // 该group创建的router不需要再显示的加入"authn.User" - - e.Start() -} -``` - -# 内置中间件 - -## Recovery - -代码位于`pkg/net/http/blademaster/recovery.go`内,用于recovery panic。会被`DefaultServer`默认注册,建议使用`NewServer`的话也将其作为首个中间件注册。 - -## Trace - -代码位于`pkg/net/http/blademaster/trace.go`内,用于trace设置,并且实现了`net/http/httptrace`的接口,能够收集官方库内的调用栈详情。会被`DefaultServer`默认注册,建议使用`NewServer`的话也将其作为第二个中间件注册。 - -## Logger - -代码位于`pkg/net/http/blademaster/logger.go`内,用于请求日志记录。会被`DefaultServer`默认注册,建议使用`NewServer`的话也将其作为第三个中间件注册。 - -## CSRF - -代码位于`pkg/net/http/blademaster/csrf.go`内,用于防跨站请求。如要使用如下: - -```go -e := bm.DefaultServer(nil) -// 挂载自适应限流中间件到 bm engine,使用默认配置 -csrf := bm.CSRF([]string{"bilibili.com"}, []string{"/a/api"}) -e.Use(csrf) -// 或者 -e.GET("/api", csrf, myHandler) -``` - -## CORS - -代码位于`pkg/net/http/blademaster/cors.go`内,用于跨域允许请求。请注意该: -1. 使用该中间件进行全局注册后,可"省略"单独为`OPTIONS`请求注册路由,如示例一。 -2. 使用该中间单独为某路由注册,需要为该路由再注册一个`OPTIONS`方法的同路径路由,如示例二。 - -示例一: -```go -e := bm.DefaultServer(nil) -// 挂载自适应限流中间件到 bm engine,使用默认配置 -cors := bm.CORS([]string{"github.com"}) -e.Use(cors) -// 该路由可以默认针对 OPTIONS /api 的跨域请求支持 -e.POST("/api", myHandler) -``` - -示例二: -```go -e := bm.DefaultServer(nil) -// 挂载自适应限流中间件到 bm engine,使用默认配置 -cors := bm.CORS([]string{"github.com"}) -// e.Use(cors) 不进行全局注册 -e.OPTIONS("/api", cors, myHandler) // 需要单独为/api进行OPTIONS方法注册 -e.POST("/api", cors, myHandler) -``` - -## 自适应限流 - -更多关于自适应限流的信息可参考:[kratos 自适应限流](ratelimit.md)。如要使用如下: - -```go -e := bm.DefaultServer(nil) -// 挂载自适应限流中间件到 bm engine,使用默认配置 -limiter := bm.NewRateLimiter(nil) -e.Use(limiter.Limit()) -// 或者 -e.GET("/api", csrf, myHandler) -``` - -# 扩展阅读 - -[bm快速开始](blademaster-quickstart.md) -[bm模块说明](blademaster-mod.md) -[bm基于pb生成](blademaster-pb.md) - diff --git a/docs/blademaster-mod.md b/docs/blademaster-mod.md deleted file mode 100644 index e8c81d704..000000000 --- a/docs/blademaster-mod.md +++ /dev/null @@ -1,88 +0,0 @@ -# Context - -以下是 blademaster 中 Context 对象结构体声明的代码片段: -```go -// Context is the most important part. It allows us to pass variables between -// middleware, manage the flow, validate the JSON of a request and render a -// JSON response for example. -type Context struct { - context.Context - - Request *http.Request - Writer http.ResponseWriter - - // flow control - index int8 - handlers []HandlerFunc - - // Keys is a key/value pair exclusively for the context of each request. - Keys map[string]interface{} - - Error error - - method string - engine *Engine -} -``` - -* 首先可以看到 blademaster 的 Context 结构体中会 embed 一个标准库中的 Context 实例,bm 中的 Context 也是直接通过该实例来实现标准库中的 Context 接口。 -* blademaster 会使用配置的 server timeout (默认1s) 作为一次请求整个过程中的超时时间,使用该context调用dao做数据库、缓存操作查询时均会将该超时时间传递下去,一旦抵达deadline,后续相关操作均会返回`context deadline exceeded`。 -* Request 和 Writer 字段用于获取当前请求的与输出响应。 -* index 和 handlers 用于 handler 的流程控制;handlers 中存储了当前请求需要执行的所有 handler,index 用于标记当前正在执行的 handler 的索引位。 -* Keys 用于在 handler 之间传递一些额外的信息。 -* Error 用于存储整个请求处理过程中的错误。 -* method 用于检查当前请求的 Method 是否与预定义的相匹配。 -* engine 字段指向当前 blademaster 的 Engine 实例。 - -以下为 Context 中所有的公开的方法: -```go -// 用于 Handler 的流程控制 -func (c *Context) Abort() -func (c *Context) AbortWithStatus(code int) -func (c *Context) Bytes(code int, contentType string, data ...[]byte) -func (c *Context) IsAborted() bool -func (c *Context) Next() - -// 用户获取或者传递请求的额外信息 -func (c *Context) RemoteIP() (cip string) -func (c *Context) Set(key string, value interface{}) -func (c *Context) Get(key string) (value interface{}, exists bool) - -// 用于校验请求的 payload -func (c *Context) Bind(obj interface{}) error -func (c *Context) BindWith(obj interface{}, b binding.Binding) error - -// 用于输出响应 -func (c *Context) Render(code int, r render.Render) -func (c *Context) Redirect(code int, location string) -func (c *Context) Status(code int) -func (c *Context) String(code int, format string, values ...interface{}) -func (c *Context) XML(data interface{}, err error) -func (c *Context) JSON(data interface{}, err error) -func (c *Context) JSONMap(data map[string]interface{}, err error) -func (c *Context) Protobuf(data proto.Message, err error) -``` - -所有方法基本上可以分为三类: - -* 流程控制 -* 额外信息传递 -* 请求处理 -* 响应处理 - -# Handler - -![handler](img/bm-handlers.png) - -初次接触`blademaster`的用户可能会对其`Handler`的流程处理产生不小的疑惑,实际上`bm`对`Handler`对处理非常简单: - -* 将`Router`模块中预先注册的`middleware`与其他`Handler`合并,放入`Context`的`handlers`字段,并将`index`字段置`0` -* 然后通过`Next()`方法一个个执行下去,部分`middleware`可能想要在过程中中断整个流程,此时可以使用`Abort()`方法提前结束处理 -* 有些`middleware`还想在所有`Handler`执行完后再执行部分逻辑,此时可以在自身`Handler`中显式调用`Next()`方法,并将这些逻辑放在调用了`Next()`方法之后 - -# 扩展阅读 - -[bm快速开始](blademaster-quickstart.md) -[bm中间件](blademaster-mid.md) -[bm基于pb生成](blademaster-pb.md) - diff --git a/docs/blademaster-pb.md b/docs/blademaster-pb.md deleted file mode 100644 index 0b693af13..000000000 --- a/docs/blademaster-pb.md +++ /dev/null @@ -1,83 +0,0 @@ -# 介绍 - -基于proto文件可以快速生成`bm`框架对应的代码,提前需要准备以下工作: - -* 安装`kratos tool protoc`工具,请看[kratos工具](kratos-tool.md) -* 编写`proto`文件,示例可参考[kratos-demo内proto文件](https://github.com/go-kratos/kratos-demo/blob/master/api/api.proto) - -### kratos工具说明 - -`kratos tool protoc`工具可以生成`warden` `bm` `swagger`对应的代码和文档,想要单独生成`bm`代码只需加上`--bm`如: - -```shell -# generate BM HTTP -kratos tool protoc --bm api.proto -``` - -### proto文件说明 - -请注意想要生成`bm`代码,需要特别在`proto`的`service`内指定`google.api.http`配置,如下: - -```go -service Demo { - rpc SayHello (HelloReq) returns (.google.protobuf.Empty); - rpc SayHelloURL(HelloReq) returns (HelloResp) { - option (google.api.http) = { // 该配置指定SayHelloURL方法对应的url - get:"/kratos-demo/say_hello" // 指定url和请求方式为GET - }; - }; -} -``` - -# 使用 - -建议在项目`api`目录下编写`proto`文件及生成对应的代码,可参考[kratos-demo内的api目录](https://github.com/go-kratos/kratos-demo/tree/master/api)。 - -执行命令后生成的`api.bm.go`代码,注意其中的`type DemoBMServer interface`和`RegisterDemoBMServer`,其中: - -* `DemoBMServer`接口,包含`proto`文件内配置了`google.api.http`选项的所有方法 -* `RegisterDemoBMServer`方法提供注册`DemoBMServer`接口的实现对象,和`bm`的`Engine`用于注册路由 -* `DemoBMServer`接口的实现,一般为`internal/service`内的业务逻辑代码,需要实现`DemoBMServer`接口 - -使用`RegisterDemoBMServer`示例代码请参考[kratos-demo内的http](https://github.com/go-kratos/kratos-demo/blob/master/internal/server/http/server.go)内的如下代码: - -```go -engine = bm.DefaultServer(hc.Server) -pb.RegisterDemoBMServer(engine, svc) -initRouter(engine) -``` - -`internal/service`内的`Service`结构实现了`DemoBMServer`接口可参考[kratos-demo内的service](https://github.com/go-kratos/kratos-demo/blob/master/internal/service/service.go)内的如下代码: - -```go -// SayHelloURL bm demo func. -func (s *Service) SayHelloURL(ctx context.Context, req *pb.HelloReq) (reply *pb.HelloResp, err error) { - reply = &pb.HelloResp{ - Content: "hello " + req.Name, - } - fmt.Printf("hello url %s", req.Name) - return -} -``` - -# 文档 - -基于同一份`proto`文件还可以生成对应的`swagger`文档,运行命令如下: - -```shell -# generate swagger -kratos tool protoc --swagger api.proto -``` - -该命令将生成对应的`swagger.json`文件,可用于`swagger`工具通过WEBUI的方式打开使用,可运行命令如下: - -```shell -kratos tool swagger serve api/api.swagger.json -``` - -# 扩展阅读 - -[bm快速开始](blademaster-quickstart.md) -[bm模块说明](blademaster-mod.md) -[bm中间件](blademaster-mid.md) - diff --git a/docs/blademaster-quickstart.md b/docs/blademaster-quickstart.md deleted file mode 100644 index d49064b50..000000000 --- a/docs/blademaster-quickstart.md +++ /dev/null @@ -1,142 +0,0 @@ -# 路由 - -进入`internal/server/http`目录下,打开`http.go`文件,其中有默认生成的`blademaster`模板。其中: - -```go -engine = bm.DefaultServer(hc.Server) -initRouter(engine) -if err := engine.Start(); err != nil { - panic(err) -} -``` - -是bm默认创建的`engine`及启动代码,我们看`initRouter`初始化路由方法,默认实现了: - -```go -func initRouter(e *bm.Engine) { - e.Ping(ping) // engine自带的"/ping"接口,用于负载均衡检测服务健康状态 - g := e.Group("/kratos-demo") // e.Group 创建一组 "/kratos-demo" 起始的路由组 - { - g.GET("/start", howToStart) // g.GET 创建一个 "kratos-demo/start" 的路由,使用GET方式请求,默认处理Handler为howToStart方法 - g.POST("start", howToStart) // g.POST 创建一个 "kratos-demo/start" 的路由,使用POST方式请求,默认处理Handler为howToStart方法 - } -} -``` - -bm的handler方法,结构如下: - -```go -func howToStart(c *bm.Context) // handler方法默认传入bm的Context对象 -``` - -### Ping - -engine自带Ping方法,用于设置`/ping`路由的handler,该路由统一提供于负载均衡服务做健康检测。服务是否健康,可自定义`ping handler`进行逻辑判断,如检测DB是否正常等。 - -```go -func ping(c *bm.Context) { - if some DB check not ok { - c.AbortWithStatus(503) - } -} -``` - -# 默认路由 - -默认路由有: - -* /metrics 用于prometheus信息采集 -* /metadata 可以查看所有注册的路由信息 - -查看加载的所有路由信息: - -```shell -curl 'http://127.0.0.1:8000/metadata' -``` - -输出: - -```json -{ - "code": 0, - "message": "0", - "ttl": 1, - "data": { - "/kratos-demo/start": { - "method": "GET" - }, - "/metadata": { - "method": "GET" - }, - "/metrics": { - "method": "GET" - }, - "/ping": { - "method": "GET" - } - } -} -``` - -# 路径参数 - -使用方式如下: - -```go -func initRouter(e *bm.Engine) { - e.Ping(ping) - g := e.Group("/kratos-demo") - { - g.GET("/start", howToStart) - - // 路径参数有两个特殊符号":"和"*" - // ":" 跟在"/"后面为参数的key,匹配两个/中间的值 或 一个/到结尾(其中不再包含/)的值 - // "*" 跟在"/"后面为参数的key,匹配从 /*开始到结尾的所有值,所有*必须写在最后且无法多个 - - // NOTE:这是不被允许的,会和 /start 冲突 - // g.GET("/:xxx") - - // NOTE: 可以拿到一个key为name的参数。注意只能匹配到/param1/felix,无法匹配/param1/felix/hao(该路径会404) - g.GET("/param1/:name", pathParam) - // NOTE: 可以拿到多个key参数。注意只能匹配到/param2/felix/hao/love,无法匹配/param2/felix或/param2/felix/hao - g.GET("/param2/:name/:value/:felid", pathParam) - // NOTE: 可以拿到一个key为name的参数 和 一个key为action的路径。 - // NOTE: 如/params3/felix/hello,action的值为"/hello" - // NOTE: 如/params3/felix/hello/hi,action的值为"/hello/hi" - // NOTE: 如/params3/felix/hello/hi/,action的值为"/hello/hi/" - g.GET("/param3/:name/*action", pathParam) - } -} - -func pathParam(c *bm.Context) { - name, _ := c.Params.Get("name") - value, _ := c.Params.Get("value") - felid, _ := c.Params.Get("felid") - action, _ := c.Params.Get("action") - path := c.RoutePath // NOTE: 获取注册的路由原始地址,如: /kratos-demo/param1/:name - c.JSONMap(map[string]interface{}{ - "name": name, - "value": value, - "felid": felid, - "action": action, - "path": path, - }, nil) -} -``` - -# 性能分析 - -启动时默认监听了`2333`端口用于`pprof`信息采集,如: - -```shell -go tool pprof http://127.0.0.1:8000/debug/pprof/profile -``` - -改变端口可以使用flag,如:`-http.perf=tcp://0.0.0.0:12333` - -# 扩展阅读 - -[bm模块说明](blademaster-mod.md) -[bm中间件](blademaster-mid.md) -[bm基于pb生成](blademaster-pb.md) - diff --git a/docs/blademaster.md b/docs/blademaster.md deleted file mode 100644 index 1915c3ed6..000000000 --- a/docs/blademaster.md +++ /dev/null @@ -1,43 +0,0 @@ -# 背景 - -在像微服务这样的分布式架构中,经常会有一些需求需要你调用多个服务,但是还需要确保服务的安全性、统一化每次的请求日志或者追踪用户完整的行为等等。要实现这些功能,你可能需要在所有服务中都设置一些相同的属性,虽然这个可以通过一些明确的接入文档来描述或者准入规范来界定,但是这么做的话还是有可能会有一些问题: - -1. 你很难让每一个服务都实现上述功能。因为对于开发者而言,他们应当注重的是实现功能。很多项目的开发者经常在一些日常开发中遗漏了这些关键点,经常有人会忘记去打日志或者去记录调用链。但是对于一些大流量的互联网服务而言,一个线上服务一旦发生故障时,即使故障时间很小,其影响面会非常大。一旦有人在关键路径上忘记路记录日志,那么故障的排除成本会非常高,那样会导致影响面进一步扩大。 -2. 事实上实现之前叙述的这些功能的成本也非常高。比如说对于鉴权(Identify)这个功能,你要是去一个服务一个服务地去实现,那样的成本也是非常高的。如果说把这个确保认证的责任分担在每个开发者身上,那样其实也会增加大家遗忘或者忽略的概率。 - -为了解决这样的问题,你可能需要一个框架来帮助你实现这些功能。比如说帮你在一些关键路径的请求上配置必要的鉴权或超时策略。那样服务间的调用会被多层中间件所过滤并检查,确保整体服务的稳定性。 - -# 设计目标 - -* 性能优异,不应该掺杂太多业务逻辑的成分 -* 方便开发使用,开发对接的成本应该尽可能地小 -* 后续鉴权、认证等业务逻辑的模块应该可以通过业务模块的开发接入该框架内 -* 默认配置已经是 production ready 的配置,减少开发与线上环境的差异性 - -# 概览 - -* 参考`gin`设计整套HTTP框架,去除`gin`中不需要的部分逻辑 -* 内置一些必要的中间件,便于业务方可以直接上手使用 - -# blademaster架构 - -![bm-arch](img/bm-arch-2-2.png) - -`blademaster`由几个非常精简的内部模块组成。其中`Router`用于根据请求的路径分发请求,`Context`包含了一个完整的请求信息,`Handler`则负责处理传入的`Context`,`Handlers`为一个列表,一个串一个地执行。 -所有的`middlerware`均以`Handler`的形式存在,这样可以保证`blademaster`自身足够精简且扩展性足够强。 - -![bm-arch](img/bm-arch-2-3.png) - -`blademaster`处理请求的模式非常简单,大部分的逻辑都被封装在了各种`Handler`中。一般而言,业务逻辑作为最后一个`Handler`。 - -正常情况下每个`Handler`按照顺序一个一个串行地执行下去,但是`Handler`中也可以中断整个处理流程,直接输出`Response`。这种模式常被用于校验登陆的`middleware`中:一旦发现请求不合法,直接响应拒绝。 - -请求处理的流程中也可以使用`Render`来辅助渲染`Response`,比如对于不同的请求需要响应不同的数据格式`JSON`、`XML`,此时可以使用不同的`Render`来简化逻辑。 - -# 扩展阅读 - -[bm快速开始](blademaster-quickstart.md) -[bm模块说明](blademaster-mod.md) -[bm中间件](blademaster-mid.md) -[bm基于pb生成](blademaster-pb.md) - diff --git a/docs/breaker.md b/docs/breaker.md deleted file mode 100644 index 2a4ff474c..000000000 --- a/docs/breaker.md +++ /dev/null @@ -1,49 +0,0 @@ -## 熔断器/Breaker -熔断器是为了当依赖的服务已经出现故障时,主动阻止对依赖服务的请求。保证自身服务的正常运行不受依赖服务影响,防止雪崩效应。 - -## kratos内置breaker的组件 -一般情况下直接使用kratos的组件时都自带了熔断逻辑,并且在提供了对应的breaker配置项。 -目前在kratos内集成熔断器的组件有: -- RPC client: pkg/net/rpc/warden/client -- Mysql client:pkg/database/sql -- Tidb client:pkg/database/tidb -- Http client:pkg/net/http/blademaster - -## 使用说明 -```go - //初始化熔断器组 - //一组熔断器公用同一个配置项,可从分组内取出单个熔断器使用。可用在比如mysql主从分离等场景。 - brkGroup := breaker.NewGroup(&breaker.Config{}) - //为每一个连接指定一个breaker - //此处假设一个客户端连接对象实例为conn - //breakName定义熔断器名称 一般可以使用连接地址 - breakName = conn.Addr - conn.breaker = brkGroup.Get(breakName) - - //在连接发出请求前判断熔断器状态 - if err = conn.breaker.Allow(); err != nil { - return - } - - //连接执行成功或失败将结果告知breaker - if(respErr != nil){ - conn.breaker.MarkFailed() - }else{ - conn.breaker.MarkSuccess() - } - -``` - -## 配置说明 -```go -type Config struct { - SwitchOff bool // 熔断器开关,默认关 false. - - K float64 //触发熔断的错误率(K = 1 - 1/错误率) - - Window xtime.Duration //统计桶窗口时间 - Bucket int //统计桶大小 - Request int64 //触发熔断的最少请求数量(请求少于该值时不会触发熔断) -} -``` - diff --git a/docs/cache-mc.md b/docs/cache-mc.md deleted file mode 100644 index 04f60f967..000000000 --- a/docs/cache-mc.md +++ /dev/null @@ -1,195 +0,0 @@ -# 开始使用 - -## 配置 - -进入项目中的configs目录,打开memcache.toml,我们可以看到: - -```toml -[Client] - name = "demo" - proto = "tcp" - addr = "127.0.0.1:11211" - active = 50 - idle = 10 - dialTimeout = "100ms" - readTimeout = "200ms" - writeTimeout = "300ms" - idleTimeout = "80s" -``` -在该配置文件中我们可以配置memcache的连接方式proto、连接地址addr、连接池的闲置连接数idle、最大连接数active以及各类超时。 - -## 初始化 - -进入项目的internal/dao目录,打开mc.go,其中: - -```go -var cfg struct { - Client *memcache.Config -} -checkErr(paladin.Get("memcache.toml").UnmarshalTOML(&mc)) -``` -使用paladin配置管理工具将上文中的memcache.toml中的配置解析为我们需要使用的配置。 - -```go -// dao dao. -type dao struct { - mc *memcache.Memcache - mcExpire int32 -} -``` - -在dao的主结构提中定义了memcache的连接池对象和过期时间。 - -```go -d = &dao{ - // memcache - mc: memcache.New(mc.Demo), - mcExpire: int32(time.Duration(mc.DemoExpire) / time.Second), -} -``` - -使用kratos/pkg/cache/memcache包的New方法进行连接池对象的初始化,需要传入上文解析的配置。 - -## Ping - -```go -// Ping ping the resource. -func (d *dao) Ping(ctx context.Context) (err error) { - return d.pingMC(ctx) -} - -func (d *dao) pingMC(ctx context.Context) (err error) { - if err = d.mc.Set(ctx, &memcache.Item{Key: "ping", Value: []byte("pong"), Expiration: 0}); err != nil { - log.Error("conn.Set(PING) error(%v)", err) - } - return -} -``` - -生成的dao层模板中自带了memcache相关的ping方法,用于为负载均衡服务的健康监测提供依据,详见[blademaster](blademaster-quickstart.md)。 - -## 关闭 - -```go -// Close close the resource. -func (d *Dao) Close() { - d.mc.Close() -} -``` - -在关闭dao层时,通过调用memcache连接池对象的Close方法,我们可以关闭该连接池,从而释放相关资源。 - -# 常用方法 - -推荐使用[memcache代码生成器](kratos-genmc.md)帮助我们生成memcache操作的相关代码。 - -以下我们来逐一解析以下kratos/pkg/cache/memcache包中提供的常用方法。 - -## 单个查询 - -```go -// CacheDemo get data from mc -func (d *Dao) CacheDemo(c context.Context, id int64) (res *Demo, err error) { - key := demoKey(id) - res = &Demo{} - if err = d.mc.Get(c, key).Scan(res); err != nil { - res = nil - if err == memcache.ErrNotFound { - err = nil - } - } - if err != nil { - prom.BusinessErrCount.Incr("mc:CacheDemo") - log.Errorv(c, log.KV("CacheDemo", fmt.Sprintf("%+v", err)), log.KV("key", key)) - return - } - return -} -``` - -如上为代码生成器生成的进行单个查询的代码,使用到mc.Get(c,key)方法获得返回值,再使用scan方法将memcache的返回值转换为golang中的类型(如string,bool, 结构体等)。 - -## 批量查询使用 - -```go -replies, err := d.mc.GetMulti(c, keys) -for _, key := range replies.Keys() { - v := &Demo{} - err = replies.Scan(key, v) -} -``` - -如上为代码生成器生成的进行批量查询的代码片段,这里使用到mc.GetMulti(c,keys)方法获得返回值,与单个查询类似地,我们需要再使用scan方法将memcache的返回值转换为我们定义的结构体。 - -## 设置KV - -```go -// AddCacheDemo Set data to mc -func (d *Dao) AddCacheDemo(c context.Context, id int64, val *Demo) (err error) { - if val == nil { - return - } - key := demoKey(id) - item := &memcache.Item{Key: key, Object: val, Expiration: d.demoExpire, Flags: memcache.FlagJSON | memcache.FlagGzip} - if err = d.mc.Set(c, item); err != nil { - prom.BusinessErrCount.Incr("mc:AddCacheDemo") - log.Errorv(c, log.KV("AddCacheDemo", fmt.Sprintf("%+v", err)), log.KV("key", key)) - return - } - return -} -``` - -如上为代码生成器生成的添加结构体进入memcache的代码,这里需要使用到的是mc.Set方法进行设置。 -这里使用的item为memcache.Item结构体,包含key, value, 超时时间(秒), Flags。 - -### Flags - - -上文添加结构体进入memcache中,使用到的flags为:memcache.FlagJSON | memcache.FlagGzip代表着:使用json作为编码方式,gzip作为压缩方式。 - -Flags的相关常量在kratos/pkg/cache/memcache包中进行定义,包含编码方式如gob, json, protobuf,和压缩方式gzip。 - -```go -const( - // Flag, 15(encoding) bit+ 17(compress) bit - - // FlagRAW default flag. - FlagRAW = uint32(0) - // FlagGOB gob encoding. - FlagGOB = uint32(1) << 0 - // FlagJSON json encoding. - FlagJSON = uint32(1) << 1 - // FlagProtobuf protobuf - FlagProtobuf = uint32(1) << 2 - // FlagGzip gzip compress. - FlagGzip = uint32(1) << 15 -) -``` - -## 删除KV - -```go -// DelCacheDemo delete data from mc -func (d *Dao) DelCacheDemo(c context.Context, id int64) (err error) { - key := demoKey(id) - if err = d.mc.Delete(c, key); err != nil { - if err == memcache.ErrNotFound { - err = nil - return - } - prom.BusinessErrCount.Incr("mc:DelCacheDemo") - log.Errorv(c, log.KV("DelCacheDemo", fmt.Sprintf("%+v", err)), log.KV("key", key)) - return - } - return -} -``` -如上为代码生成器生成的从memcache中删除KV的代码,这里需要使用到的是mc.Delete方法。 -和查询时类似地,当memcache中不存在参数中的key时,会返回error为memcache.ErrNotFound。如果不需要处理这种error,可以参考上述代码将返回出去的error置为nil。 - -# 扩展阅读 - -[memcache代码生成器](kratos-genmc.md) -[redis模块说明](cache-redis.md) - diff --git a/docs/cache-redis.md b/docs/cache-redis.md deleted file mode 100644 index 898e419df..000000000 --- a/docs/cache-redis.md +++ /dev/null @@ -1,181 +0,0 @@ -# 开始使用 - -## 配置 - -进入项目中的configs目录,打开redis.toml,我们可以看到: - -```toml -[Client] - name = "kratos-demo" - proto = "tcp" - addr = "127.0.0.1:6389" - idle = 10 - active = 10 - dialTimeout = "1s" - readTimeout = "1s" - writeTimeout = "1s" - idleTimeout = "10s" -``` - -在该配置文件中我们可以配置redis的连接方式proto、连接地址addr、连接池的闲置连接数idle、最大连接数active以及各类超时。 - -## 初始化 - -进入项目的internal/dao目录,打开redis.go,其中: - -```go -var cfg struct { - Client *memcache.Config -} -checkErr(paladin.Get("redis.toml").UnmarshalTOML(&rc)) -``` -使用paladin配置管理工具将上文中的redis.toml中的配置解析为我们需要使用的配置。 - -```go -// Dao dao. -type Dao struct { - redis *redis.Pool - redisExpire int32 -} -``` - -在dao的主结构提中定义了redis的连接池对象和过期时间。 - -```go -d = &dao{ - // redis - redis: redis.NewPool(rc.Demo), - redisExpire: int32(time.Duration(rc.DemoExpire) / time.Second), -} -``` - -使用kratos/pkg/cache/redis包的NewPool方法进行连接池对象的初始化,需要传入上文解析的配置。 - -## Ping - -```go -// Ping ping the resource. -func (d *dao) Ping(ctx context.Context) (err error) { - return d.pingRedis(ctx) -} - -func (d *dao) pingRedis(ctx context.Context) (err error) { - conn := d.redis.Get(ctx) - defer conn.Close() - if _, err = conn.Do("SET", "ping", "pong"); err != nil { - log.Error("conn.Set(PING) error(%v)", err) - } - return -} -``` - -生成的dao层模板中自带了redis相关的ping方法,用于为负载均衡服务的健康监测提供依据,详见[blademaster](blademaster-quickstart.md)。 - -## 关闭 - -```go -// Close close the resource. -func (d *Dao) Close() { - d.redis.Close() -} -``` - -在关闭dao层时,通过调用redis连接池对象的Close方法,我们可以关闭该连接池,从而释放相关资源。 - -# 常用方法 - -## 发送单个命令 Do - -```go -// DemoIncrby . -func (d *dao) DemoIncrby(c context.Context, pid int) (err error) { - cacheKey := keyDemo(pid) - conn := d.redis.Get(c) - defer conn.Close() - if _, err = conn.Do("INCRBY", cacheKey, 1); err != nil { - log.Error("DemoIncrby conn.Do(INCRBY) key(%s) error(%v)", cacheKey, err) - } - return -} -``` -如上为向redis server发送单个命令的用法示意。这里需要使用redis连接池的Get方法获取一个redis连接conn,再使用conn.Do方法即可发送一条指令。 -注意,在使用该连接完毕后,需要使用conn.Close方法将该连接关闭。 - -## 批量发送命令 Pipeline - -kratos/pkg/cache/redis包除了支持发送单个命令,也支持批量发送命令(redis pipeline),比如: - -```go -// DemoIncrbys . -func (d *dao) DemoIncrbys(c context.Context, pid int) (err error) { - cacheKey := keyDemo(pid) - conn := d.redis.Get(c) - defer conn.Close() - if err = conn.Send("INCRBY", cacheKey, 1); err != nil { - return - } - if err = conn.Send("EXPIRE", cacheKey, d.redisExpire); err != nil { - return - } - if err = conn.Flush(); err != nil { - log.Error("conn.Flush error(%v)", err) - return - } - for i := 0; i < 2; i++ { - if _, err = conn.Receive(); err != nil { - log.Error("conn.Receive error(%v)", err) - return - } - } - return -} -``` - -和发送单个命令类似地,这里需要使用redis连接池的Get方法获取一个redis连接conn,在使用该连接完毕后,需要使用conn.Close方法将该连接关闭。 - -这里使用conn.Send方法将命令写入客户端的buffer(缓冲区)中,使用conn.Flush将客户端的缓冲区内的命令打包发送到redis server。redis server按顺序返回的reply可以使用conn.Receive方法进行接收和处理。 - - -## 返回值转换 - -kratos/pkg/cache/redis包中也提供了Scan方法将redis server的返回值转换为golang类型。 - -除此之外,kratos/pkg/cache/redis包提供了大量返回值转换的快捷方式: - -### 单个查询 - -单个查询可以使用redis.Uint64/Int64/Float64/Int/String/Bool/Bytes进行返回值的转换,比如: - -```go -// GetDemo get -func (d *Dao) GetDemo(ctx context.Context, key string) (string, error) { - conn := d.redis.Get(ctx) - defer conn.Close() - return redis.String(conn.Do("GET", key)) -} -``` - -### 批量查询 - -批量查询时候,可以使用redis.Int64s,Ints,Strings,ByteSlices方法转换如MGET,HMGET,ZRANGE,SMEMBERS等命令的返回值。 -还可以使用StringMap, IntMap, Int64Map方法转换HGETALL命令的返回值,比如: - -```go -// HGETALLDemo get -func (d *Dao) HGETALLDemo(c context.Context, pid int64) (res map[string]int64, err error) { - var ( - key = keyDemo(pid) - conn = d.redis.Get(c) - ) - defer conn.Close() - if res, err = redis.Int64Map(conn.Do("HGETALL", key)); err != nil { - log.Error("HGETALL %v failed error(%v)", key, err) - } - return -} -``` - -# 扩展阅读 - -[memcache模块说明](cache-mc.md) - diff --git a/docs/cache.md b/docs/cache.md deleted file mode 100644 index a1e484035..000000000 --- a/docs/cache.md +++ /dev/null @@ -1,20 +0,0 @@ -# 背景 - -我们需要统一的cache包,用于进行各类缓存操作。 - -# 概览 - -* 缓存操作均使用连接池,保证较快的数据读写速度且提高系统的安全可靠性。 - -# Memcache - -提供protobuf,gob,json序列化方式,gzip的memcache接口 - -[memcache模块说明](cache-mc.md) - -# Redis - -提供redis操作的各类接口以及各类将redis server返回值转换为golang类型的快捷方法。 - -[redis模块说明](cache-redis.md) - diff --git a/docs/config-paladin.md b/docs/config-paladin.md deleted file mode 100644 index 28d8c1cc9..000000000 --- a/docs/config-paladin.md +++ /dev/null @@ -1,118 +0,0 @@ -# Paladin SDK - -## 配置模块化 -进行配置的模块化是为了更好地管理配置,尽可能避免由修改配置带来的失误。 -在配置种类里,可以看到其实 环境配置 和 应用配置 已经由平台进行管理化。 -我们通常业务里只用配置 业务配置 和 在线配置 就可以了,之前我们大部分都是单个文件配置,而为了更好管理我们需要按类型进行拆分配置文件。 - -例如: - -| 名称 | 说明 | -|:------|:------| -| application.toml | 在线配置 | -| mysql.toml | 业务db配置 | -| hbase.toml | 业务hbase配置 | -| memcache.toml | 业务mc配置 | -| redis.toml | 业务redis配置 | -| http.toml | 业务http client/server/auth配置 | -| grpc.toml | 业务grpc client/server配置 | - -## 使用方式 - -paladin 是一个config SDK客户端,包括了remote、file、mock几个抽象功能,方便使用本地文件或者远程配置中心,并且集成了对象自动reload功能。 - -### 远程配置中心 -可以通过环境变量注入,例如:APP_ID/DEPLOY_ENV/ZONE/HOSTNAME,然后通过paladin实现远程配置中心SDK进行配合使用。 - -### 指定本地文件: -```shell -./cmd -conf=/data/conf/app/demo.toml -# or multi file -./cmd -conf=/data/conf/app/ -``` - -### mock配置文件 -```go -func TestMain(t *testing.M) { - mock := make(map[string]string]) - mock["application.toml"] = ` - demoSwitch = false - demoNum = 100 - demoAPI = "xxx" - ` - paladin.DefaultClient = paladin.NewMock(mock) -} -``` - -### example main -```go -// main.go -func main() { - flag.Parse() - // 初始化paladin - if err := paladin.Init(); err != nil { - panic(err) - } - log.Init(nil) // debug flag: log.dir={path} - defer log.Close() -} -``` - -### example HTTP/gRPC -```toml -# http.toml -[server] - addr = "0.0.0.0:9000" - timeout = "1s" - -``` - -```go -// server.go -func NewServer() { - // 默认配置用nil,这时读取HTTP/gRPC构架中的flag或者环境变量(可能是docker注入的环境变量,默认端口:8000/9000) - engine := bm.DefaultServer(nil) - - // 除非自己要替换了配置,用http.toml - var bc struct { - Server *bm.ServerConfig - } - if err := paladin.Get("http.toml").UnmarshalTOML(&bc); err != nil { - // 不存在时,将会为nil使用默认配置 - if err != paladin.ErrNotExist { - panic(err) - } - } - engine := bm.DefaultServer(bc.Server) -} -``` - -### example Service(在线配置热加载配置) -```go -# service.go -type Service struct { - ac *paladin.Map -} - -func New() *Service { - // paladin.Map 通过atomic.Value支持自动热加载 - var ac = new(paladin.TOML) - if err := paladin.Watch("application.toml", ac); err != nil { - panic(err) - } - s := &Service{ - ac: ac, - } - return s -} - -func (s *Service) Test() { - sw, err := s.ac.Get("switch").Bool() - if err != nil { - // TODO - } - - // or use default value - sw := paladin.Bool(s.ac.Get("switch"), false) -} -``` diff --git a/docs/config.md b/docs/config.md deleted file mode 100644 index f27244ab9..000000000 --- a/docs/config.md +++ /dev/null @@ -1,48 +0,0 @@ -# config - -## 介绍 -初看起来,配置管理可能很简单,但是这其实是不稳定的一个重要来源。 -即变更管理导致的故障,我们目前基于配置中心(config-service)的部署方式,二进制文件的发布与配置文件的修改是异步进行的,每次变更配置,需要重新构建发布版。 -由此,我们整体对配置文件进行梳理,对配置进行模块化,以及方便易用的paladin config sdk。 - -## 环境配置 - -| flag | env | remark | -|:----------|:----------|:------| -| region | REGION | 部署地区,sh-上海、gz-广州、bj-北京 | -| zone | ZONE | 分布区域,sh001-上海核心、sh004-上海嘉定 | -| deploy.env | DEPLOY_ENV | dev-开发、fat1-功能、uat-集成、pre-预发、prod-生产 | -| deploy.color | DEPLOY_COLOR | 服务颜色,blue(测试feature染色请求) | -| - | HOSTNAME | 主机名,xxx-hostname | - -全局公用环境变量,通常为部署环境配置,由系统、发布系统或supervisor进行环境变量注入,并不用进行例外配置,如果是开发过程中则可以通过flag注入进行运行测试。 - -## 应用配置 - -| flag | env | default | remark | -|:----------|:----------|:-------------|:------| -| appid | APP_ID | - | 应用ID | -| http | HTTP | tcp://0.0.0.0:8000/?timeout=1s | http 监听端口 | -| http.perf | HTTP_PERF | tcp://0.0.0.0:2233/?timeout=1s | http perf 监听端口 | -| grpc | GRPC | tcp://0.0.0.0:9000/?timeout=1s&idle_timeout=60s | grpc 监听端口 | -| grpc.target | - | - | 指定服务运行:
-grpc.target=demo.service=127.0.0.1:9000
-grpc.target=demo.service=127.0.0.2:9000 | -| discovery.nodes | DISCOVERY_NODES | - | 服务发现节点:127.0.0.1:7171,127.0.0.2:7171 | -| log.v | LOG_V | 0 | 日志级别:
DEBUG:0 INFO:1 WARN:2 ERROR:3 FATAL:4 | -| log.stdout | LOG_STDOUT | false | 是否标准输出:true、false| -| log.dir | LOG_DIR | - | 日志文件目录,如果配置会输出日志到文件,否则不输出日志文件 | -| log.agent | LOG_AGENT | - | 日志采集agent:
unixpacket:///var/run/lancer/collector_tcp.sock?timeout=100ms&chan=1024 | -| log.module | LOG_MODULE | - | 指定field信息 format: file=1,file2=2. | -| log.filter | LOG_FILTER | - | 过虑敏感信息 format: field1,field2. | - -基本为一些应用相关的配置信息,通常发布系统和supervisor都有对应的部署环境进行配置注入,并不用进行例外配置,如果开发过程中可以通过flag进行注入运行测试。 - -## 业务配置 -Redis、MySQL等业务组件,可以使用静态的配置文件来初始化,根据应用业务集群进行配置。 - -## 在线配置 -需要在线读取、变更的配置信息,比如某个业务开关,可以实现配置reload实时更新。 - -## 扩展阅读 - -[paladin配置sdk](config-paladin.md) - diff --git a/docs/database-hbase.md b/docs/database-hbase.md deleted file mode 100644 index 327092b7f..000000000 --- a/docs/database-hbase.md +++ /dev/null @@ -1,51 +0,0 @@ -# database/hbase - -## 说明 -Hbase Client,进行封装加入了链路追踪和统计。 - -## 配置 -需要指定hbase集群的zookeeper地址。 -``` -config := &hbase.Config{Zookeeper: &hbase.ZKConfig{Addrs: []string{"localhost"}}} -client := hbase.NewClient(config) -``` - -## 使用方式 -``` -package main - -import ( - "context" - "fmt" - - "github.com/go-kratos/kratos/pkg/database/hbase" -) - -func main() { - config := &hbase.Config{Zookeeper: &hbase.ZKConfig{Addrs: []string{"localhost"}}} - client := hbase.NewClient(config) - - // - values := map[string]map[string][]byte{"name": {"firstname": []byte("hello"), "lastname": []byte("world")}} - ctx := context.Background() - - // 写入信息 - // table: user - // rowkey: user1 - // values["family"] = columns - _, err := client.PutStr(ctx, "user", "user1", values) - if err != nil { - panic(err) - } - - // 读取信息 - // table: user - // rowkey: user1 - result, err := client.GetStr(ctx, "user", "user1") - if err != nil { - panic(err) - } - fmt.Printf("%v", result) -} -``` - diff --git a/docs/database-mysql-orm.md b/docs/database-mysql-orm.md deleted file mode 100644 index e29d5b1b5..000000000 --- a/docs/database-mysql-orm.md +++ /dev/null @@ -1,42 +0,0 @@ -# 开始使用 - -## 配置 - -进入项目中的configs目录,mysql.toml,我们可以看到: - -```toml -[demo] - addr = "127.0.0.1:3306" - dsn = "{user}:{password}@tcp(127.0.0.1:3306)/{database}?timeout=1s&readTimeout=1s&writeTimeout=1s&parseTime=true&loc=Local&charset=utf8mb4,utf8" - readDSN = ["{user}:{password}@tcp(127.0.0.2:3306)/{database}?timeout=1s&readTimeout=1s&writeTimeout=1s&parseTime=true&loc=Local&charset=utf8mb4,utf8","{user}:{password}@tcp(127.0.0.3:3306)/{database}?timeout=1s&readTimeout=1s&writeTimeout=1s&parseTime=true&loc=Local&charset=utf8,utf8mb4"] - active = 20 - idle = 10 - idleTimeout ="4h" - queryTimeout = "200ms" - execTimeout = "300ms" - tranTimeout = "400ms" -``` - -在该配置文件中我们可以配置mysql的读和写的dsn、连接地址addr、连接池的闲置连接数idle、最大连接数active以及各类超时。 - -如果配置了readDSN,在进行读操作的时候会优先使用readDSN的连接。 - -## 初始化 - -进入项目的internal/dao目录,打开db.go,其中: - -```go -var cfg struct { - Client *sql.Config -} -checkErr(paladin.Get("db.toml").UnmarshalTOML(&dc)) -``` -使用paladin配置管理工具将上文中的db.toml中的配置解析为我们需要使用db的相关配置。 - -# TODO:补充常用方法 - -# 扩展阅读 - -[tidb模块说明](database-tidb.md) -[hbase模块说明](database-hbase.md) - diff --git a/docs/database-mysql.md b/docs/database-mysql.md deleted file mode 100644 index ee289f60c..000000000 --- a/docs/database-mysql.md +++ /dev/null @@ -1,195 +0,0 @@ -# 开始使用 - -## 配置 - -进入项目中的configs目录,mysql.toml,我们可以看到: - -```toml -[demo] - addr = "127.0.0.1:3306" - dsn = "{user}:{password}@tcp(127.0.0.1:3306)/{database}?timeout=1s&readTimeout=1s&writeTimeout=1s&parseTime=true&loc=Local&charset=utf8mb4,utf8" - readDSN = ["{user}:{password}@tcp(127.0.0.2:3306)/{database}?timeout=1s&readTimeout=1s&writeTimeout=1s&parseTime=true&loc=Local&charset=utf8mb4,utf8","{user}:{password}@tcp(127.0.0.3:3306)/{database}?timeout=1s&readTimeout=1s&writeTimeout=1s&parseTime=true&loc=Local&charset=utf8,utf8mb4"] - active = 20 - idle = 10 - idleTimeout ="4h" - queryTimeout = "200ms" - execTimeout = "300ms" - tranTimeout = "400ms" -``` - -在该配置文件中我们可以配置mysql的读和写的dsn、连接地址addr、连接池的闲置连接数idle、最大连接数active以及各类超时。 - -如果配置了readDSN,在进行读操作的时候会优先使用readDSN的连接。 - -## 初始化 - -进入项目的internal/dao目录,打开db.go,其中: - -```go -var cfg struct { - Client *sql.Config -} -checkErr(paladin.Get("db.toml").UnmarshalTOML(&dc)) -``` -使用paladin配置管理工具将上文中的db.toml中的配置解析为我们需要使用db的相关配置。 - -```go -// Dao dao. -type Dao struct { - db *sql.DB -} -``` - -在dao的主结构提中定义了mysql的连接池对象。 - -```go -d = &dao{ - db: sql.NewMySQL(dc.Demo), -} -``` - -使用kratos/pkg/database/sql包的NewMySQL方法进行连接池对象的初始化,需要传入上文解析的配置。 - -## Ping - -```go -// Ping ping the resource. -func (d *dao) Ping(ctx context.Context) (err error) { - return d.db.Ping(ctx) -} -``` - -生成的dao层模板中自带了mysql相关的ping方法,用于为负载均衡服务的健康监测提供依据,详见[blademaster](blademaster-quickstart.md)。 - -## 关闭 - -```go -// Close close the resource. -func (d *dao) Close() { - d.db.Close() -} -``` - -在关闭dao层时,通过调用mysql连接池对象的Close方法,我们可以关闭该连接池,从而释放相关资源。 - -# 常用方法 - -## 单个查询 - -```go -// GetDemo 用户角色 -func (d *dao) GetDemo(c context.Context, did int64) (demo int8, err error) { - err = d.db.QueryRow(c, _getDemoSQL, did).Scan(&demo) - if err != nil && err != sql.ErrNoRows { - log.Error("d.GetDemo.Query error(%v)", err) - return - } - return demo, nil -} -``` - -db.QueryRow方法用于返回最多一条记录的查询,在QueryRow方法后使用Scan方法即可将mysql的返回值转换为Golang的数据类型。 - -当mysql查询不到对应数据时,会返回sql.ErrNoRows,如果不需处理,可以参考如上代码忽略此error。 - -## 批量查询 - -```go -// ResourceLogs ResourceLogs. -func (d *dao) GetDemos(c context.Context, dids []int64) (demos []int8, err error) { - rows, err := d.db.Query(c, _getDemosSQL, dids) - if err != nil { - log.Error("query error(%v)", err) - return - } - defer rows.Close() - for rows.Next() { - var tmpD int8 - if err = rows.Scan(&tmpD); err != nil { - log.Error("scan demo log error(%v)", err) - return - } - demos = append(demos, tmpD) - } - return -} -``` - -db.Query方法一般用于批量查询的场景,返回*sql.Rows和error信息。 -我们可以使用rows.Next()方法获得下一行的返回结果,并且配合使用rows.Scan()方法将该结果转换为Golang的数据类型。当没有下一行时,rows.Next方法将返回false,此时循环结束。 - -注意,在使用完毕rows对象后,需要调用rows.Close方法关闭连接,释放相关资源。 - -## 执行语句 - -```go -// DemoExec exec -func (d *Dao) DemoExec(c context.Context, id int64) (rows int64, err error) { - res, err := d.db.Exec(c, _demoUpdateSQL, id) - if err != nil { - log.Error("db.DemoExec.Exec(%s) error(%v)", _demoUpdateSQL, err) - return - } - return res.RowsAffected() -} -``` - -执行UPDATE/DELETE/INSERT语句时,使用db.Exec方法进行语句执行,返回*sql.Result和error信息: - -```go - -// A Result summarizes an executed SQL command. -type Result interface { - LastInsertId() (int64, error) - RowsAffected() (int64, error) -} -``` - -Result接口支持获取影响行数和LastInsertId(一般用于获取Insert语句插入数据库后的主键ID) - - -## 事务 - -kratos/pkg/database/sql包支持事务操作,具体操作示例如下: - -开启一个事务: - -```go -tx := d.db.Begin() -if err = tx.Error; err != nil { - log.Error("db begin transcation failed, err=%+v", err) - return -} -``` - -在事务中执行语句: - -```go -res, err := tx.Exec(_demoSQL, did) -if err != nil { - return -} -rows := res.RowsAffected() -``` - -提交事务: - -```go -if err = tx.Commit().Error; err!=nil{ - log.Error("db commit transcation failed, err=%+v", err) -} -``` - -回滚事务: - -```go -if err = tx.Rollback().Error; err!=nil{ - log.Error("db rollback failed, err=%+v", rollbackErr) -} -``` - -# 扩展阅读 - -- [tidb模块说明](database-tidb.md) -- [hbase模块说明](database-hbase.md) - diff --git a/docs/database-tidb.md b/docs/database-tidb.md deleted file mode 100644 index e69de29bb..000000000 diff --git a/docs/database.md b/docs/database.md deleted file mode 100644 index f8cb3ebf2..000000000 --- a/docs/database.md +++ /dev/null @@ -1,19 +0,0 @@ -# database/sql - -## 背景 -数据库驱动,进行封装加入了熔断、链路追踪和统计,以及链路超时。 -通常数据模块都写在`internal/dao`目录中,并提供对应的数据访问接口。 - -## MySQL -MySQL数据库驱动,支持读写分离、context、timeout、trace和统计功能,以及错误熔断防止数据库雪崩。 -[mysql client](database-mysql.md) -[mysql client orm](database-mysql-orm.md) - -## HBase -HBase客户端,支持trace、slowlog和统计功能。 -[hbase client](database-hbase.md) - -## TiDB -TiDB客户端,支持服务发现和熔断功能。 -[tidb client](database-tidb.md) - diff --git a/docs/design/kratos-v2.md b/docs/design/kratos-v2.md new file mode 100644 index 000000000..6aa6293f9 --- /dev/null +++ b/docs/design/kratos-v2.md @@ -0,0 +1,14 @@ +# Kratos v2 Kit Design + +MaoJian + +Last updated: December 25, 2020 + +## Abstract +kratos v1 基础库主要专注在各类功能的细节实现,比如 gRPC 的负载均衡,熔断器等一系列微服务需要的功能。 + +## Background + +## Proposal + +## Implementation diff --git a/docs/ecode.md b/docs/ecode.md deleted file mode 100644 index 10d8f0e8b..000000000 --- a/docs/ecode.md +++ /dev/null @@ -1,102 +0,0 @@ -# ecode - -## 背景 -错误码一般被用来进行异常传递,且需要具有携带`message`文案信息的能力。 - -## 错误码之Codes - -在`kratos`里,错误码被设计成`Codes`接口,声明如下[代码位置](https://github.com/go-kratos/kratos/blob/master/pkg/ecode/ecode.go): - -```go -// Codes ecode error interface which has a code & message. -type Codes interface { - // sometimes Error return Code in string form - // NOTE: don't use Error in monitor report even it also work for now - Error() string - // Code get error code. - Code() int - // Message get code message. - Message() string - //Detail get error detail,it may be nil. - Details() []interface{} -} - -// A Code is an int error code spec. -type Code int -``` - -可以看到该接口一共有四个方法,且`type Code int`结构体实现了该接口。 - -### 注册message - -一个`Code`错误码可以对应一个`message`,默认实现会从全局变量`_messages`中获取,业务可以将自定义`Code`对应的`message`通过调用`Register`方法的方式传递进去,如: - -```go -cms := map[int]string{ - 0: "很好很强大!", - -304: "啥都没变啊~", - -404: "啥都没有啊~", -} -ecode.Register(cms) - -fmt.Println(ecode.OK.Message()) // 输出:很好很强大! -``` - -注意:`map[int]string`类型并不是绝对,比如有业务要支持多语言的场景就可以扩展为类似`map[int]LangStruct`的结构,因为全局变量`_messages`是`atomic.Value`类型,只需要修改对应的`Message`方法实现即可。 - -### Details - -`Details`接口为`gRPC`预留,`gRPC`传递异常会将服务端的错误码pb序列化之后赋值给`Details`,客户端拿到之后反序列化得到,具体可阅读`status`的实现: -1. `ecode`包内的`Status`结构体实现了`Codes`接口[代码位置](https://github.com/go-kratos/kratos/blob/master/pkg/ecode/status.go) -2. `warden/internal/status`包内包装了`ecode.Status`和`grpc.Status`进行互相转换的方法[代码位置](https://github.com/go-kratos/kratos/blob/master/pkg/net/rpc/warden/internal/status/status.go) -3. `warden`的`client`和`server`则使用转换方法将`gRPC`底层返回的`error`最终转换为`ecode.Status` [代码位置](https://github.com/go-kratos/kratos/blob/master/pkg/net/rpc/warden/client.go#L162) - -## 转换为ecode - -错误码转换有以下两种情况: -1. 因为框架传递错误是靠`ecode`错误码,比如bm框架返回的`code`字段默认就是数字,那么客户端接收到如`{"code":-404}`的话,可以使用`ec := ecode.Int(-404)`或`ec := ecode.String("-404")`来进行转换。 -2. 在项目中`dao`层返回一个错误码,往往返回参数类型建议为`error`而不是`ecode.Codes`,因为`error`更通用,那么上层`service`就可以使用`ec := ecode.Cause(err)`进行转换。 - -## 判断 - -错误码判断是否相等: -1. `ecode`与`ecode`判断使用:`ecode.Equal(ec1, ec2)` -2. `ecode`与`error`判断使用:`ecode.EqualError(ec, err)` - -## 使用工具生成 - -使用proto协议定义错误码,格式如下: - -```proto -// user.proto -syntax = "proto3"; - -package ecode; - -enum UserErrCode { - UserUndefined = 0; // 因protobuf协议限制必须存在!!!无意义的0,工具生成代码时会忽略该参数 - UserNotLogin = 123; // 正式错误码 -} -``` - -需要注意以下几点: - -1. 必须是enum类型,且名字规范必须以"ErrCode"结尾,如:UserErrCode -2. 因为protobuf协议限制,第一个enum值必须为无意义的0 - -使用`kratos tool protoc --ecode user.proto`进行生成,生成如下代码: - -```go -package ecode - -import ( - "github.com/go-kratos/kratos/pkg/ecode" -) - -var _ ecode.Codes - -// UserErrCode -var ( - UserNotLogin = ecode.New(123); -) -``` diff --git a/docs/images/alipay.png b/docs/images/alipay.png new file mode 100644 index 0000000000000000000000000000000000000000..4497460fcda21843ca288032f3d2b4d24326ea3f GIT binary patch literal 42462 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
PSdQBUa5$M!8`ph}!!x|E{ioNa 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 diff --git a/docs/img/kratos.png b/docs/img/kratos.png deleted file mode 100644 index aa4d1cd91e07d0e7fbc88c4f1211d587bcac7e7c..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 7429 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

1k6itNau* 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 Rdw&1`002ovPDHLkV1lOjZK?nO diff --git a/docs/img/ratelimit-benchmark-up-1.png b/docs/img/ratelimit-benchmark-up-1.png deleted file mode 100644 index 7f8d3a866d56f28ffced6cb87eb8d9c2c7180050..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 676504 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 }RxC%)H4explHG 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

6iUO|JsP5FzY#B0MXd^BCXAvl5cq% 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

ycr0cB#VF(_0BM3C3wtw5{^J8-&n9Flud!S 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%0DaKI*ZNWT4x3_?snJ>7X* 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�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 diff --git a/docs/img/ratelimit-rolling-window.png b/docs/img/ratelimit-rolling-window.png deleted file mode 100644 index 168937d3953c815c14e91fcbb7c3faa2a4167abd..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 19131 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 diff --git a/docs/img/zipkin.jpg b/docs/img/zipkin.jpg deleted file mode 100644 index 8fca3794df32618d0fe3096dcc4dff2326a71f61..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 65836 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%0DEX+o8k~i5 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 diff --git a/docs/index.html b/docs/index.html deleted file mode 100644 index 94fa7b465..000000000 --- a/docs/index.html +++ /dev/null @@ -1,30 +0,0 @@ - - - - - Kratos Documentation - - - - - - -

- - - - - - diff --git a/docs/install.md b/docs/install.md deleted file mode 100644 index 4ff3e2057..000000000 --- a/docs/install.md +++ /dev/null @@ -1,66 +0,0 @@ -# 安装 - -1.安装protoc二进制文件 - -``` -下载地址:https://github.com/google/protobuf/releases -mv bin/protoc /usr/local/bin/ -mv -r include/google /usr/local/include/ -``` - -2.安装protobuf库文件 - -``` -go get -u github.com/golang/protobuf/proto -``` - -3.安装goprotobuf插件 - -``` -go get github.com/golang/protobuf/protoc-gen-go -``` - -4.安装gogoprotobuf插件和依赖 - -``` -//gogo -go get github.com/gogo/protobuf/protoc-gen-gogo - -//gofast -go get github.com/gogo/protobuf/protoc-gen-gofast - -//依赖 -go get github.com/gogo/protobuf/proto -go get github.com/gogo/protobuf/gogoproto -``` - -5.安装框架依赖 - -``` -# grpc (或者git clone https://github.com/grpc/grpc-go 然后复制到google.golang.org/grpc) -go get -u google.golang.org/grpc - -# genproto (或者git clone https://github.com/google/go-genproto 然后复制到google.golang.org/genproto) -go get google.golang.org/genproto/... -``` - -6.安装kratos tool - -``` -go get -u github.com/go-kratos/kratos/tool/kratos -cd $GOPATH/src -kratos new kratos-demo --proto -``` - -7.运行 - -``` -cd kratos-demo/cmd -go build -./cmd -conf ../configs -``` - -打开浏览器访问:[http://localhost:8000/kratos-demo/start](http://localhost:8000/kratos-demo/start),你会看到输出了`Golang 大法好 !!!` - -[kratos工具](kratos-tool.md) - diff --git a/docs/kratos-genbts.md b/docs/kratos-genbts.md deleted file mode 100644 index ca3e74430..000000000 --- a/docs/kratos-genbts.md +++ /dev/null @@ -1,27 +0,0 @@ -### kratos tool genbts - -> 缓存回源代码生成 - -在internal/dao/dao.go中添加mc缓存interface定义,可以指定对应的[注解参数](../../tool/kratos-gen-bts/README.md); -并且在接口前面添加`go:generate kratos tool genbts`; -然后在当前目录执行`go generate`,可以看到自动生成的dao.bts.go代码。 - -### 回源模板 -```go -//go:generate kratos tool genbts -type _bts interface { - // bts: -batch=2 -max_group=20 -batch_err=break -nullcache=&Demo{ID:-1} -check_null_code=$.ID==-1 - Demos(c context.Context, keys []int64) (map[int64]*Demo, error) - // bts: -sync=true -nullcache=&Demo{ID:-1} -check_null_code=$.ID==-1 - Demo(c context.Context, key int64) (*Demo, error) - // bts: -paging=true - Demo1(c context.Context, key int64, pn int, ps int) (*Demo, error) - // bts: -nullcache=&Demo{ID:-1} -check_null_code=$.ID==-1 - None(c context.Context) (*Demo, error) -} -``` - -### 参考 - -也可以参考完整的testdata例子:kratos/tool/kratos-gen-bts/testdata - diff --git a/docs/kratos-genmc.md b/docs/kratos-genmc.md deleted file mode 100644 index d83378c4d..000000000 --- a/docs/kratos-genmc.md +++ /dev/null @@ -1,68 +0,0 @@ -### kratos tool genmc - -> 缓存代码生成 - -在internal/dao/dao.go中添加mc缓存interface定义,可以指定对应的[注解参数](../../tool/kratos-gen-mc/README.md); -并且在接口前面添加`go:generate kratos tool genmc`; -然后在当前目录执行`go generate`,可以看到自动生成的mc.cache.go代码。 - -### 缓存模板 -```go -//go:generate kratos tool genmc -type _mc interface { - // mc: -key=demoKey - CacheDemos(c context.Context, keys []int64) (map[int64]*Demo, error) - // mc: -key=demoKey - CacheDemo(c context.Context, key int64) (*Demo, error) - // mc: -key=keyMid - CacheDemo1(c context.Context, key int64, mid int64) (*Demo, error) - // mc: -key=noneKey - CacheNone(c context.Context) (*Demo, error) - // mc: -key=demoKey - CacheString(c context.Context, key int64) (string, error) - - // mc: -key=demoKey -expire=d.demoExpire -encode=json - AddCacheDemos(c context.Context, values map[int64]*Demo) error - // mc: -key=demo2Key -expire=d.demoExpire -encode=json - AddCacheDemos2(c context.Context, values map[int64]*Demo, tp int64) error - // 这里也支持自定义注释 会替换默认的注释 - // mc: -key=demoKey -expire=d.demoExpire -encode=json|gzip - AddCacheDemo(c context.Context, key int64, value *Demo) error - // mc: -key=keyMid -expire=d.demoExpire -encode=gob - AddCacheDemo1(c context.Context, key int64, value *Demo, mid int64) error - // mc: -key=noneKey - AddCacheNone(c context.Context, value *Demo) error - // mc: -key=demoKey -expire=d.demoExpire - AddCacheString(c context.Context, key int64, value string) error - - // mc: -key=demoKey - DelCacheDemos(c context.Context, keys []int64) error - // mc: -key=demoKey - DelCacheDemo(c context.Context, key int64) error - // mc: -key=keyMid - DelCacheDemo1(c context.Context, key int64, mid int64) error - // mc: -key=noneKey - DelCacheNone(c context.Context) error -} - -func demoKey(id int64) string { - return fmt.Sprintf("art_%d", id) -} - -func demo2Key(id, tp int64) string { - return fmt.Sprintf("art_%d_%d", id, tp) -} - -func keyMid(id, mid int64) string { - return fmt.Sprintf("art_%d_%d", id, mid) -} - -func noneKey() string { - return "none" -} -``` - -### 参考 - -也可以参考完整的testdata例子:kratos/tool/kratos-gen-mc/testdata - diff --git a/docs/kratos-protoc.md b/docs/kratos-protoc.md deleted file mode 100644 index 78273fa3e..000000000 --- a/docs/kratos-protoc.md +++ /dev/null @@ -1,28 +0,0 @@ -### kratos tool protoc - -```shell -# generate all -kratos tool protoc api.proto -# generate gRPC -kratos tool protoc --grpc api.proto -# generate BM HTTP -kratos tool protoc --bm api.proto -# generate ecode -kratos tool protoc --ecode api.proto -# generate swagger -kratos tool protoc --swagger api.proto -``` - -执行生成如 `api.pb.go/api.bm.go/api.swagger.json/api.ecode.go` 的对应文件,需要注意的是:`ecode`生成有固定规则,需要首先是`enum`类型,且`enum`名字要以`ErrCode`结尾,如`enum UserErrCode`。详情可见:[example](https://github.com/go-kratos/kratos/tree/master/example/protobuf) - -> 该工具在Windows/Linux下运行,需提前安装好 [protobuf](https://github.com/google/protobuf) 工具 - -`kratos tool protoc`本质上是拼接好了`protoc`命令然后进行执行,在执行时会打印出对应执行的`protoc`命令,如下可见: - -```shell -protoc --proto_path=$GOPATH --proto_path=$GOPATH/github.com/go-kratos/kratos/third_party --proto_path=. --bm_out=:. api.proto -protoc --proto_path=$GOPATH --proto_path=$GOPATH/github.com/go-kratos/kratos/third_party --proto_path=. --gofast_out=plugins=grpc:. api.proto -protoc --proto_path=$GOPATH --proto_path=$GOPATH/github.com/go-kratos/kratos/third_party --proto_path=. --bswagger_out=:. api.proto -protoc --proto_path=$GOPATH --proto_path=$GOPATH/github.com/go-kratos/kratos/third_party --proto_path=. --ecode_out=:. api.proto -``` - diff --git a/docs/kratos-swagger.md b/docs/kratos-swagger.md deleted file mode 100644 index 6d28b3f2d..000000000 --- a/docs/kratos-swagger.md +++ /dev/null @@ -1,8 +0,0 @@ -### kratos tool swagger -```shell -kratos tool swagger serve api/api.swagger.json -``` -执行命令后,浏览器会自动打开swagger文档地址。 -同时也可以查看更多的 [go-swagger](https://github.com/go-swagger/go-swagger) 官方参数进行使用。 - - diff --git a/docs/kratos-tool.md b/docs/kratos-tool.md deleted file mode 100644 index 3bb45a4f2..000000000 --- a/docs/kratos-tool.md +++ /dev/null @@ -1,106 +0,0 @@ -# 介绍 - -kratos包含了一批好用的工具集,比如项目一键生成、基于proto生成http&grpc代码,生成缓存回源代码,生成memcache执行代码,生成swagger文档等。 - -# 获取工具 - -执行以下命令,即可快速安装好`kratos`工具 -```shell -go get -u github.com/go-kratos/kratos/tool/kratos -``` - -那么接下来让我们快速开始熟悉工具的用法~ - -# kratos本体 - -`kratos`是所有工具集的本体,就像`go`一样,拥有执行各种子工具的能力,如`go build`和`go tool`。先让我们看看`-h`的输出: - -``` -NAME: - kratos - kratos tool - -USAGE: - kratos [global options] command [command options] [arguments...] - -VERSION: - 0.0.1 - -COMMANDS: - new, n create new project - build, b kratos build - run, r kratos run - tool, t kratos tool - version, v kratos version - self-upgrade kratos self-upgrade - help, h Shows a list of commands or help for one command - -GLOBAL OPTIONS: - --help, -h show help - --version, -v print the version -``` - -可以看到`kratos`有如:`new` `build` `run` `tool`等在内的COMMANDS,那么接下来一一演示如何使用。 - -# kratos new - -`kratos new`是快速创建一个项目的命令,执行如下: - -```shell -kratos new kratos-demo -``` - -即可快速在当前目录生成一个叫`kratos-demo`的项目。此外还支持指定owner和path,如下: - -```shell -kratos new kratos-demo -o YourName -d YourPath -``` - -注意,`kratos new`默认会生成通过 protobuf 定义的`grpc`和`bm`示例代码的,如只生成bm请加`--http`,如下: - -```shell -kratos new kratos-demo -o YourName -d YourPath --http -``` - -如只生成grpc请加`--grpc`,如下: - -```shell -kratos new kratos-demo -o YourName -d YourPath --grpc -``` - -> 特别注意,如果不是MacOS系统,需要自己进行手动安装protoc,用于生成的示例项目`api`目录下的`proto`文件并不会自动生成对应的`.pb.go`和`.bm.go`文件。 - -> 也可以参考以下说明进行生成:[protoc说明](protoc.md) - -# kratos build & run - -`kratos build`和`kratos run`是`go build`和`go run`的封装,可以在当前项目任意目录进行快速运行进行调试,并无特别用途。 - -# kratos tool - -`kratos tool`是基于proto生成http&grpc代码,生成缓存回源代码,生成memcache执行代码,生成swagger文档等工具集,先看下的执行效果: - -``` -kratos tool - -protoc(已安装): 快速方便生成pb.go的protoc封装,windows、Linux请先安装protoc工具 Author(kratos) [2019/10/31] -genbts(已安装): 缓存回源逻辑代码生成器 Author(kratos) [2019/10/31] -testcli(已安装): 测试代码生成 Author(kratos) [2019/09/09] -genmc(已安装): mc缓存代码生成 Author(kratos) [2019/07/23] -swagger(已安装): swagger api文档 Author(goswagger.io) [2019/05/05] - -安装工具: kratos tool install demo -执行工具: kratos tool demo -安装全部工具: kratos tool install all -全部升级: kratos tool upgrade all -``` - -> 小小说明:如未安装工具,第一次运行也可自动安装,不需要特别执行install - -目前已经集成的工具有: - -* [kratos](kratos-tool.md) 为本体工具,只用于安装更新使用; -* [protoc](kratos-protoc.md) 用于快速生成gRPC、HTTP、Swagger文件,该命令Windows,Linux用户需要手动安装 protobuf 工具; -* [swagger](kratos-swagger.md) 用于显示自动生成的HTTP API接口文档,通过 `kratos tool swagger serve api/api.swagger.json` 可以查看文档; -* [genmc](kratos-genmc.md) 用于自动生成memcached缓存代码; -* [genbts](kratos-genbts.md) 用于生成缓存回源代码生成,如果miss则调用回源函数从数据源获取,然后塞入缓存; - diff --git a/docs/log-agent.md b/docs/log-agent.md deleted file mode 100644 index e69de29bb..000000000 diff --git a/docs/logger.md b/docs/logger.md deleted file mode 100644 index 6c30bb1c1..000000000 --- a/docs/logger.md +++ /dev/null @@ -1,34 +0,0 @@ -# 日志基础库 - -## 概览 -基于[zap](https://github.com/uber-go/zap)的field方式实现的高性能log库,提供Info、Warn、Error日志级别; -并提供了context支持,方便打印环境信息以及日志的链路追踪,在框架中都通过field方式实现,避免format日志带来的性能消耗。 - -## 配置选项 - -| flag | env | type | remark | -|:----------|:----------|:-------------:|:------| -| log.v | LOG_V | int | 日志级别:DEBUG:0 INFO:1 WARN:2 ERROR:3 FATAL:4 | -| log.stdout | LOG_STDOUT | bool | 是否标准输出:true、false| -| log.dir | LOG_DIR | string | 日志文件目录,如果配置会输出日志到文件,否则不输出日志文件 | -| log.agent | LOG_AGENT | string | 日志采集agent:unixpacket:///var/run/lancer/collector_tcp.sock?timeout=100ms&chan=1024 | -| log.module | LOG_MODULE | string | 指定field信息 format: file=1,file2=2. | -| log.filter | LOG_FILTER | string | 过虑敏感信息 format: field1,field2. | - -## 使用方式 -```go -func main() { - // 解析flag - flag.Parse() - // 初始化日志模块 - log.Init(nil) - // 打印日志 - log.Info("hi:%s", "kratos") - log.Infoc(Context.TODO(), "hi:%s", "kratos") - log.Infov(Context.TODO(), log.KVInt("key1", 100), log.KVString("key2", "test value") -} -``` - -## 扩展阅读 -* [log-agent](log-agent.md) - diff --git a/docs/protoc.md b/docs/protoc.md deleted file mode 100644 index 4676154c0..000000000 --- a/docs/protoc.md +++ /dev/null @@ -1,26 +0,0 @@ -# protoc - -`protobuf`是Google官方出品的一种轻便高效的结构化数据存储格式,可以用于结构化数据串行化,或者说序列化。它很适合做数据存储或 RPC 数据交换格式。可用于通讯协议、数据存储等领域的语言无关、平台无关、可扩展的序列化结构数据格式。 - -使用`protobuf`,需要先书写`.proto`文件,然后编译该文件。编译`proto`文件则需要使用到官方的`protoc`工具,安装文档请参看:[google官方protoc工具](https://github.com/protocolbuffers/protobuf#protocol-compiler-installation)。 - -注意:`protoc`是用于编辑`proto`文件的工具,它并不具备生成对应语言代码的能力,所以正常都是`protoc`配合对应语言的代码生成工具来使用,如Go语言的[gogo protobuf](https://github.com/gogo/protobuf),请先点击按文档说明安装。 - -安装好对应工具后,我们可以进入`api`目录,执行如下命令: - -```shell -export $KRATOS_HOME = kratos路径 -export $KRATOS_DEMO = 项目路径 - -// 生成:api.pb.go -protoc -I$GOPATH/src:$KRATOS_HOME/tool/protobuf/pkg/extensions:$KRATOS_DEMO/api --gogofast_out=plugins=grpc:$KRATOS_DEMO/api $KRATOS_DEMO/api/api.proto - -// 生成:api.bm.go -protoc -I$GOPATH/src:$KRATOS_HOME/tool/protobuf/pkg/extensions:$KRATOS_DEMO/api --bm_out=$KRATOS_DEMO/api $KRATOS_DEMO/api/api.proto - -// 生成:api.swagger.json -protoc -I$GOPATH/src:$KRATOS_HOME/tool/protobuf/pkg/extensions:$KRATOS_DEMO/api --bswagger_out=$KRATOS_DEMO/api $KRATOS_DEMO/api/api.proto -``` - -请注意替换`/Users/felix/work/go/src`目录为你本地开发环境对应GOPATH目录,其中`--gogofast_out`意味着告诉`protoc`工具需要使用`gogo protobuf`的工具生成代码。 - diff --git a/docs/quickstart.md b/docs/quickstart.md deleted file mode 100644 index 0cf5c0b4d..000000000 --- a/docs/quickstart.md +++ /dev/null @@ -1,71 +0,0 @@ -# 快速开始 - -**在安装之前,请确认您的开发环境中正确安装了[golang](https://golang.org/), [git](https://git-scm.com/book/en/v2/Getting-Started-Installing-Git), 和[protoc](https://grpc.io/docs/protoc-installation/)** - -创建kratos项目,可以使用`kratos`工具,如下: - -```shell -go get -u github.com/go-kratos/kratos/tool/kratos -cd $GOPATH/src -kratos new kratos-demo -``` - -根据提示可以快速创建项目,如[kratos-demo](https://github.com/go-kratos/kratos-demo)就是通过工具创建生成。目录结构如下: - -``` -├── CHANGELOG.md -├── OWNERS -├── README.md -├── api # api目录为对外保留的proto文件及生成的pb.go文件 -│   ├── api.bm.go -│   ├── api.pb.go # 通过go generate生成的pb.go文件 -│   ├── api.proto -│   └── client.go -├── cmd -│   └── main.go # cmd目录为main所在 -├── configs # configs为配置文件目录 -│   ├── application.toml # 应用的自定义配置文件,可能是一些业务开关如:useABtest = true -│   ├── db.toml # db相关配置 -│   ├── grpc.toml # grpc相关配置 -│   ├── http.toml # http相关配置 -│   ├── memcache.toml # memcache相关配置 -│   └── redis.toml # redis相关配置 -├── go.mod -├── go.sum -└── internal # internal为项目内部包,包括以下目录: -│   ├── dao # dao层,用于数据库、cache、MQ、依赖某业务grpc|http等资源访问 -│   │   ├── dao.bts.go -│   │   ├── dao.go -│   │   ├── db.go -│   │   ├── mc.cache.go -│   │   ├── mc.go -│   │   └── redis.go -│   ├── di # 依赖注入层 采用wire静态分析依赖 -│   │   ├── app.go -│   │   ├── wire.go # wire 声明 -│   │   └── wire_gen.go # go generate 生成的代码 -│   ├── model # model层,用于声明业务结构体 -│   │   └── model.go -│   ├── server # server层,用于初始化grpc和http server -│   │   ├── grpc # grpc层,用于初始化grpc server和定义method -│   │   │   └── server.go -│   │   └── http # http层,用于初始化http server和声明handler -│   │   └── server.go -│   └── service # service层,用于业务逻辑处理,且为方便http和grpc共用方法,建议入参和出参保持grpc风格,且使用pb文件生成代码 -│   └── service.go -└── test # 测试资源层 用于存放测试相关资源数据 如docker-compose配置 数据库初始化语句等 - └── docker-compose.yaml -``` - -生成后可直接运行如下: - -```shell -cd kratos-demo/cmd -go build -./cmd -conf ../configs -``` - -打开浏览器访问:[http://localhost:8000/kratos-demo/start](http://localhost:8000/kratos-demo/start),你会看到输出了`Golang 大法好 !!!` - -[kratos工具](kratos-tool.md) - diff --git a/docs/ratelimit.md b/docs/ratelimit.md deleted file mode 100644 index 17f58c6aa..000000000 --- a/docs/ratelimit.md +++ /dev/null @@ -1,57 +0,0 @@ -# 自适应限流保护 - -kratos 借鉴了 Sentinel 项目的自适应限流系统,通过综合分析服务的 cpu 使用率、请求成功的 qps 和请求成功的 rt 来做自适应限流保护。 - - -## 核心目标 - -* 自动嗅探负载和 qps,减少人工配置 -* 削顶,保证超载时系统不被拖垮,并能以高水位 qps 继续运行 - - -## 限流规则 - -### 指标介绍 - -| 指标名称 | 指标含义 | -| -------- | ------------------------------------------------------------- | -| cpu | 最近 1s 的 CPU 使用率均值,使用滑动平均计算,采样周期是 250ms | -| inflight | 当前处理中正在处理的请求数量 | -| pass | 请求处理成功的量 | -| rt | 请求成功的响应耗时 | - - -### 滑动窗口 - -在自适应限流保护中,采集到的指标的时效性非常强,系统只需要采集最近一小段时间内的 qps、rt 即可,对于较老的数据,会自动丢弃。为了实现这个效果,kratos 使用了滑动窗口来保存采样数据。 - -![ratelimit-rolling-window](img/ratelimit-rolling-window.png) - -如上图,展示了一个具有两个桶(bucket)的滑动窗口(rolling window)。整个滑动窗口用来保存最近 1s 的采样数据,每个小的桶用来保存 500ms 的采样数据。 -当时间流动之后,过期的桶会自动被新桶的数据覆盖掉,在图中,在 1000-1500ms 时,bucket 1 的数据因为过期而被丢弃,之后 bucket 3 的数据填到了窗口的头部。 - - -### 限流公式 - -判断是否丢弃当前请求的算法如下: - -`cpu > 800 AND (Now - PrevDrop) < 1s AND (MaxPass * MinRt * windows / 1000) < InFlight` - -MaxPass 表示最近 5s 内,单个采样窗口中最大的请求数。 -MinRt 表示最近 5s 内,单个采样窗口中最小的响应时间。 -windows 表示一秒内采样窗口的数量,默认配置中是 5s 50 个采样,那么 windows 的值为 10。 - -## 压测报告 - -场景1,请求以每秒增加1个的速度不停上升,压测效果如下: - -![ratelimit-benchmark-up-1](img/ratelimit-benchmark-up-1.png) - -左测是没有限流的压测效果,右侧是带限流的压测效果。 -可以看到,没有限流的场景里,系统在 700qps 时开始抖动,在 1k qps 时被拖垮,几乎没有新的请求能被放行,然而在使用限流之后,系统请求能够稳定在 600 qps 左右,rt 没有暴增,服务也没有被打垮,可见,限流有效的保护了服务。 - - -## 参考资料 - -[Sentinel 系统自适应限流](https://github.com/alibaba/Sentinel/wiki/%E7%B3%BB%E7%BB%9F%E8%87%AA%E9%80%82%E5%BA%94%E9%99%90%E6%B5%81) - diff --git a/docs/trace.md b/docs/trace.md deleted file mode 100644 index 34cd5e4a9..000000000 --- a/docs/trace.md +++ /dev/null @@ -1,42 +0,0 @@ -# 背景 - -当代的互联网的服务,通常都是用复杂的、大规模分布式集群来实现的。互联网应用构建在不同的软件模块集上,这些软件模块,有可能是由不同的团队开发、可能使用不同的编程语言来实现、有可能布在了几千台服务器,横跨多个不同的数据中心。因此,就需要一些可以帮助理解系统行为、用于分析性能问题的工具。 - -# 概览 - -* kratos内部的trace基于opentracing语义 -* 使用protobuf协议描述trace结构 -* 全链路支持(gRPC/HTTP/MySQL/Redis/Memcached等) - -## 参考文档 - -[opentracing](https://github.com/opentracing-contrib/opentracing-specification-zh/blob/master/specification.md) -[dapper](https://bigbully.github.io/Dapper-translation/) - -# 使用 - -kratos本身不提供整套`trace`数据方案,但在`net/trace/report.go`内声明了`repoter`接口,可以简单的集成现有开源系统,比如:`zipkin`和`jaeger`。 - -### zipkin使用 - -可以看[zipkin](https://github.com/go-kratos/kratos/tree/master/pkg/net/trace/zipkin)的协议上报实现,具体使用方式如下: - -1. 前提是需要有一套自己搭建的`zipkin`集群 -2. 在业务代码的`main`函数内进行初始化,代码如下: - -```go -// 忽略其他代码 -import "github.com/go-kratos/kratos/pkg/net/trace/zipkin" -// 忽略其他代码 -func main(){ - // 忽略其他代码 - zipkin.Init(&zipkin.Config{ - Endpoint: "http://localhost:9411/api/v2/spans", - }) - // 忽略其他代码 -} -``` - -### zipkin效果图 - -![zipkin](img/zipkin.jpg) diff --git a/docs/ut-support.md b/docs/ut-support.md deleted file mode 100644 index 789d25f48..000000000 --- a/docs/ut-support.md +++ /dev/null @@ -1,500 +0,0 @@ -## 单元测试辅助工具 -在单元测试中,我们希望每个测试用例都是独立的。这时候就需要Stub, Mock, Fakes等工具来帮助我们进行用例和依赖之间的隔离。 - -同时通过对错误情况的 Mock 也可以帮我们检查代码多个分支结果,从而提高覆盖率。 - -以下工具已加入到 Kratos 框架 go modules,可以借助 testgen 代码生成器自动生成部分工具代码,请放心食用。更多使用方法还欢迎大家多多探索。 - -### GoConvey -GoConvey是一套针对golang语言的BDD类型的测试框架。提供了良好的管理和执行测试用例的方式,包含丰富的断言函数,而且同时有测试执行和报告Web界面的支持。 - -#### 使用特性 -为了更好的使用 GoConvey 来编写和组织测试用例,需要注意以下几点特性: - -1. Convey方法和So方法的使用 -> - Convey方法声明了一种规格的组织,每个组织内包含一句描述和一个方法。在方法内也可以嵌套其他Convey语句和So语句。 -```Go -// 顶层Convey方法,需引入*testing.T对象 -Convey(description string, t *testing.T, action func()) - -// 其他嵌套Convey方法,无需引入*testing.T对象 -Convey(description string, action func()) -``` -注:同一Scope下的Convey语句描述不可以相同! -> - So方法是断言方法,用于对执行结果进行比对。GoConvey官方提供了大量断言,同时也可以自定义自己的断言([戳这里了解官方文档](https://github.com/smartystreets/goconvey/wiki/Assertions)) -```Go -// A=B断言 -So(A, ShouldEqual, B) - -// A不为空断言 -So(A, ShouldNotBeNil) -``` - -2. 执行次序 -> 假设有以下Convey伪代码,执行次序将为A1B2A1C3。将Convey方法类比树的结点的话,整体执行类似树的遍历操作。 -> 所以Convey A部分可在组织测试用例时,充当“Setup”的方法。用于初始化等一些操作。 -```Go -Convey伪代码 -Convey A - So 1 - Convey B - So 2 - Convey C - So 3 -``` - -3. Reset方法 -> GoConvey提供了Reset方法来进行“Teardown”的操作。用于执行完测试用例后一些状态的回收,连接关闭等操作。Reset方法不可与顶层Convey语句在同层。 -```Go -// Reset -Reset func(action func()) -``` -假设有以下带有Reset方法的伪代码,同层Convey语句执行完后均会执行同层的Reset方法。执行次序为A1B2C3EA1D4E。 -```Go -Convey A - So 1 - Convey B - So 2 - Convey C - So 3 - Convey D - So 4 - Reset E -``` - -4. 自然语言逻辑到测试用例的转换 -> 在了解了Convey方法的特性和执行次序后,我们可以通过这些性质把对一个方法的测试用例按照日常逻辑组织起来。尤其建议使用Given-When-Then的形式来组织 -> - 比较直观的组织示例 -```Go -Convey("Top-level", t, func() { - - // Setup 工作,在本层内每个Convey方法执行前都会执行的部分: - db.Open() - db.Initialize() - - Convey("Test a query", func() { - db.Query() - // TODO: assertions here - }) - - Convey("Test inserts", func() { - db.Insert() - // TODO: assertions here - }) - - Reset(func() { - // Teardown工作,在本层内每个Convey方法执行完后都会执行的部分: - db.Close() - }) - -}) -``` -> - 定义单独的包含Setup和Teardown的帮助方法 -```Go -package main - -import ( - "database/sql" - "testing" - - _ "github.com/lib/pq" - . "github.com/smartystreets/goconvey/convey" -) - -// 帮助方法,将原先所需的处理方法以参数(f)形式传入 -func WithTransaction(db *sql.DB, f func(tx *sql.Tx)) func() { - return func() { - // Setup工作 - tx, err := db.Begin() - So(err, ShouldBeNil) - - Reset(func() { - // Teardown工作 - /* Verify that the transaction is alive by executing a command */ - _, err := tx.Exec("SELECT 1") - So(err, ShouldBeNil) - - tx.Rollback() - }) - - // 调用传入的闭包做实际的事务处理 - f(tx) - } -} - -func TestUsers(t *testing.T) { - db, err := sql.Open("postgres", "postgres://localhost?sslmode=disable") - if err != nil { - panic(err) - } - - Convey("Given a user in the database", t, WithTransaction(db, func(tx *sql.Tx) { - _, err := tx.Exec(`INSERT INTO "Users" ("id", "name") VALUES (1, 'Test User')`) - So(err, ShouldBeNil) - - Convey("Attempting to retrieve the user should return the user", func() { - var name string - - data := tx.QueryRow(`SELECT "name" FROM "Users" WHERE "id" = 1`) - err = data.Scan(&name) - - So(err, ShouldBeNil) - So(name, ShouldEqual, "Test User") - }) - })) -} -``` - -#### 使用建议 -强烈建议使用 [testgen](ut-testgen.md) 进行测试用例的生成,生成后每个方法将包含一个符合以下规范的正向用例。 - -用例规范: -1. 每个方法至少包含一个测试方法(命名为Test[PackageName][FunctionName]) -2. 每个测试方法包含一个顶层Convey语句,仅在此引入admin *testing.T类型的对象,在该层进行变量声明。 -3. 每个测试方法不同的用例用Convey方法组织 -4. 每个测试用例的一组断言用一个Convey方法组织 -5. 使用convey.C保持上下文一致 - -### MonkeyPatching - -#### 特性和使用条件 -1. Patch()对任何无接收者的方法均有效 -2. PatchInstanceMethod()对有接收者的包内/私有方法无法工作(因使用到了反射机制)。可以采用给私有方法的下一级打补丁,或改为无接收者的方法,或将方法转为公有 - -#### 适用场景(建议) -项目代码中上层对下层包依赖时,下层包方法Mock(例如service层对dao层方法依赖时) -基础库(MySql, Memcache, Redis)错误Mock -其他标准库,基础库以及第三方包方法Mock - -#### 使用示例 -1. 上层包对下层包依赖示例 -Service层对Dao层依赖: - -```GO -// 原方法 -func (s *Service) realnameAlipayApply(c context.Context, mid int64) (info *model.RealnameAlipayApply, err error) { - if info, err = s.mbDao.RealnameAlipayApply(c, mid); err != nil { - return - } - ... - return -} - -// 测试方法 -func TestServicerealnameAlipayApply(t *testing.T) { - convey.Convey("realnameAlipayApply", t, func(ctx convey.C) { - ... - ctx.Convey("When everything goes positive", func(ctx convey.C) { - guard := monkey.PatchInstanceMethod(reflect.TypeOf(s.mbDao), "RealnameAlipayApply", func(_ *dao.Dao, _ context.Context, _ int64) (*model.RealnameAlipayApply, error) { - return nil, nil - }) - defer guard.Unpatch() - info, err := s.realnameAlipayApply(c, mid) - ctx.Convey("Then err should be nil,info should not be nil", func(ctx convey.C) { - ctx.So(info, convey.ShouldNotBeNil) - ctx.So(err, convey.ShouldBeNil) - }) - }) - }) -} -``` -2. 基础库错误Mock示例 - -```Go -// 原方法(部分) -func (d *Dao) BaseInfoCache(c context.Context, mid int64) (info *model.BaseInfo, err error) { - ... - conn := d.mc.Get(c) - defer conn.Close() - item, err := conn.Get(key) - if err != nil { - log.Error("conn.Get(%s) error(%v)", key, err) - return - } - ... - return -} - -// 测试方法(错误Mock部分) -func TestDaoBaseInfoCache(t *testing.T) { - convey.Convey("BaseInfoCache", t, func(ctx convey.C) { - ... - Convey("When conn.Get gets error", func(ctx convey.C) { - guard := monkey.PatchInstanceMethod(reflect.TypeOf(d.mc), "Get", func(_ *memcache.Pool, _ context.Context) memcache.Conn { - return memcache.MockWith(memcache.ErrItemObject) - }) - defer guard.Unpatch() - _, err := d.BaseInfoCache(c, mid) - ctx.Convey("Error should be equal to memcache.ErrItemObject", func(ctx convey.C) { - ctx.So(err, convey.ShouldEqual, memcache.ErrItemObject) - }) - }) - }) -} -``` - -#### 注意事项 -- Monkey非线程安全 -- Monkey无法针对Inline方法打补丁,在测试时可以使用go test -gcflags=-l来关闭inline编译的模式(一些简单的go inline介绍戳这里) -- Monkey在一些面向安全不允许内存页写和执行同时进行的操作系统上无法工作 -- 更多详情请戳:https://github.com/bouk/monkey - - - -### Gock——HTTP请求Mock工具 - -#### 特性和使用条件 - -#### 工作原理 -1. 截获任意通过 http.DefaultTransport或者自定义http.Transport对外的http.Client请求 -2. 以“先进先出”原则将对外需求和预定义好的HTTP Mock池中进行匹配 -3. 如果至少一个Mock被匹配,将按照2中顺序原则组成Mock的HTTP返回 -4. 如果没有Mock被匹配,若实际的网络可用,将进行实际的HTTP请求。否则将返回错误 - -#### 特性 -- 内建帮助工具实现JSON/XML简单Mock -- 支持持久的、易失的和TTL限制的Mock -- 支持HTTP Mock请求完整的正则表达式匹配 -- 可通过HTTP方法,URL参数,请求头和请求体匹配 -- 可扩展和可插件化的HTTP匹配规则 -- 具备在Mock和实际网络模式之间切换的能力 -- 具备过滤和映射HTTP请求到正确的Mock匹配的能力 -- 支持映射和过滤可以更简单的掌控Mock -- 通过使用http.RoundTripper接口广泛兼容HTTP拦截器 -- 可以在任意net/http兼容的Client上工作 -- 网络延迟模拟(beta版本) -- 无其他依赖 - -#### 适用场景(建议) -任何需要进行HTTP请求的操作,建议全部用Gock进行Mock,以减少对环境的依赖。 - -使用示例: -1. net/http 标准库 HTTP 请求Mock - -```Go -import gock "gopkg.in/h2non/gock.v1" - -// 原方法 - func (d *Dao) Upload(c context.Context, fileName, fileType string, expire int64, body io.Reader) (location string, err error) { - ... - resp, err = d.bfsClient.Do(req) //d.bfsClient类型为*http.client - ... - if resp.StatusCode != http.StatusOK { - ... - } - header = resp.Header - code = header.Get("Code") - if code != strconv.Itoa(http.StatusOK) { - ... - } - ... - return -} - -// 测试方法 -func TestDaoUpload(t *testing.T) { - convey.Convey("Upload", t, func(ctx convey.C) { - ... - // d.client 类型为 *http.client 根据Gock包描述需要设置http.Client的Transport情况。也可在TestMain中全局设置,则所有的HTTP请求均通过Gock来解决 - d.client.Transport = gock.DefaultTransport // !注意:进行httpMock前需要对http 请求进行拦截,否则Mock失败 - // HTTP请求状态和Header都正确的Mock - ctx.Convey("When everything is correct", func(ctx convey.C) { - httpMock("PUT", url).Reply(200).SetHeaders(map[string]string{ - "Code": "200", - "Location": "SomePlace", - }) - location, err := d.Upload(c, fileName, fileType, expire, body) - ctx.Convey("Then err should be nil.location should not be nil.", func(ctx convey.C) { - ctx.So(err, convey.ShouldBeNil) - ctx.So(location, convey.ShouldNotBeNil) - }) - }) - ... - // HTTP请求状态错误Mock - ctx.Convey("When http request status != 200", func(ctx convey.C) { - d.client.Transport = gock.DefaultTransport - httpMock("PUT", url).Reply(404) - _, err := d.Upload(c, fileName, fileType, expire, body) - ctx.Convey("Then err should not be nil", func(ctx convey.C) { - ctx.So(err, convey.ShouldNotBeNil) - }) - }) - // HTTP请求Header中Code值错误Mock - ctx.Convey("When http request Code in header != 200", func(ctx convey.C) { - d.client.Transport = gock.DefaultTransport - httpMock("PUT", url).Reply(404).SetHeaders(map[string]string{ - "Code": "404", - "Location": "SomePlace", - }) - _, err := d.Upload(c, fileName, fileType, expire, body) - ctx.Convey("Then err should not be nil", func(ctx convey.C) { - ctx.So(err, convey.ShouldNotBeNil) - }) - }) - - // 由于同包内有其他进行实际HTTP请求的测试。所以再每次用例结束后,进行现场恢复(关闭Gock设置默认的Transport) - ctx.Reset(func() { - gock.OffAll() - d.client.Transport = http.DefaultClient.Transport - }) - - - }) -} - -func httpMock(method, url string) *gock.Request { - r := gock.New(url) - r.Method = strings.ToUpper(method) - return r -} -``` -2. blademaster库HTTP请求Mock - -```Go -// 原方法 -func (d *Dao) SendWechatToGroup(c context.Context, chatid, msg string) (err error) { - ... - if err = d.client.Do(c, req, &res); err != nil { - ... - } - if res.Code != 0 { - ... - } - return -} - -// 测试方法 -func TestDaoSendWechatToGroup(t *testing.T) { - convey.Convey("SendWechatToGroup", t, func(ctx convey.C) { - ... - // 根据Gock包描述需要设置bm.Client的Transport情况。也可在TestMain中全局设置,则所有的HTTP请求均通过Gock来解决。 - // d.client 类型为 *bm.client - d.client.SetTransport(gock.DefaultTransport) // !注意:进行httpMock前需要对http 请求进行拦截,否则Mock失败 - // HTTP请求状态和返回内容正常Mock - ctx.Convey("When everything gose postive", func(ctx convey.C) { - httpMock("POST", _sagaWechatURL+"/appchat/send").Reply(200).JSON(`{"code":0,"message":"0"}`) - err := d.SendWechatToGroup(c, d.c.WeChat.ChatID, msg) - ... - }) - // HTTP请求状态错误Mock - ctx.Convey("When http status != 200", func(ctx convey.C) { - httpMock("POST", _sagaWechatURL+"/appchat/send").Reply(404) - err := d.SendWechatToGroup(c, d.c.WeChat.ChatID, msg) - ... - }) - // HTTP请求返回值错误Mock - ctx.Convey("When http response code != 0", func(ctx convey.C) { - httpMock("POST", _sagaWechatURL+"/appchat/send").Reply(200).JSON(`{"code":-401,"message":"0"}`) - err := d.SendWechatToGroup(c, d.c.WeChat.ChatID, msg) - ... - }) - // 由于同包内有其他进行实际HTTP请求的测试。所以再每次用例结束后,进行现场恢复(关闭Gock设置默认的Transport)。 - ctx.Reset(func() { - gock.OffAll() - d.client.SetTransport(http.DefaultClient.Transport) - }) - }) -} - -func httpMock(method, url string) *gock.Request { - r := gock.New(url) - r.Method = strings.ToUpper(method) - return r -} -``` - -#### 注意事项 -- Gock不是完全线程安全的 -- 如果执行并发代码,在配置Gock和解释定制的HTTP clients时,要确保Mock已经事先声明好了来避免不需要的竞争机制 -- 更多详情请戳:https://github.com/h2non/gock - - -### GoMock - -#### 使用条件 -只能对公有接口(interface)定义的代码进行Mock,并仅能在测试过程中进行 - -#### 使用方法 -- 官方安装使用步骤 -```shell -## 获取GoMock包和自动生成Mock代码工具mockgen -go get github.com/golang/mock/gomock -go install github.com/golang/mock/mockgen - -## 生成mock文件 -## 方法1:生成对应文件下所有interface -mockgen -source=path/to/your/interface/file.go - -## 方法2:生成对应包内指定多个interface,并用逗号隔开 -mockgen database/sql/driver Conn,Driver - -## 示例: -mockgen -destination=$GOPATH/kratos/app/xxx/dao/dao_mock.go -package=dao kratos/app/xxx/dao DaoInterface -``` -- testgen 使用步骤(GoMock生成功能已集成在Creater工具中,无需额外安装步骤即可直接使用) -```shell -## 直接给出含有接口类型定义的包路径,生成Mock文件将放在包目录下一级mock/pkgName_mock.go中 -./creater --m mock absolute/path/to/your/pkg -``` -- 测试代码内使用方法 - -```Go -// 测试用例内直接使用 -// 需引入的包 -import ( - ... - "github.com/otokaze/mock/gomock" - ... -) - -func TestPkgFoo(t *testing.T) { - convey.Convey("Foo", t, func(ctx convey.C) { - ... - ctx.Convey("Mock Interface to test", func(ctx convey.C) { - // 1. 使用gomock.NewController新增一个控制器 - mockCtrl := gomock.NewController(t) - // 2. 测试完成后关闭控制器 - defer mockCtrl.Finish() - // 3. 以控制器为参数生成Mock对象 - yourMock := mock.NewMockYourClient(mockCtrl) - // 4. 使用Mock对象替代原代码中的对象 - yourClient = yourMock - // 5. 使用EXPECT().方法名(方法参数).Return(返回值)来构造所需输入/输出 - yourMock.EXPECT().YourMethod(gomock.Any()).Return(nil) - res:= Foo(params) - ... - }) - ... - }) -} - -// 可以利用Convey执行顺序方式适当调整以简化代码 -func TestPkgFoo(t *testing.T) { - convey.Convey("Foo", t, func(ctx convey.C) { - ... - mockCtrl := gomock.NewController(t) - yourMock := mock.NewMockYourClient(mockCtrl) - ctx.Convey("Mock Interface to test1", func(ctx convey.C) { - yourMock.EXPECT().YourMethod(gomock.Any()).Return(nil) - ... - }) - ctx.Convey("Mock Interface to test2", func(ctx convey.C) { - yourMock.EXPECT().YourMethod(args).Return(res) - ... - }) - ... - ctx.Reset(func(){ - mockCtrl.Finish() - }) - }) -} -``` - -#### 适用场景(建议) -1. gRPC中的Client接口 -2. 也可改造现有代码构造Interface后使用(具体可配合Creater的功能进行Interface和Mock的生成) -3. 任何对接口中定义方法依赖的场景 - -#### 注意事项 -- 如有Mock文件在包内,在执行单元测试时Mock代码会被识别进行测试。请注意Mock文件的放置。 -- 更多详情请戳:https://github.com/golang/mock \ No newline at end of file diff --git a/docs/ut-testcli.md b/docs/ut-testcli.md deleted file mode 100644 index 65490e110..000000000 --- a/docs/ut-testcli.md +++ /dev/null @@ -1,152 +0,0 @@ -## testcli UT运行环境构建工具 -基于 docker-compose 实现跨平台跨语言环境的容器依赖管理方案,以解决运行ut场景下的 (mysql, redis, mc)容器依赖问题。 - -*这个是testing/lich的二进制工具版本(Go请直接使用库版本:github.com/go-kratos/kratos/pkg/testing/lich)* - -### 功能和特性 -- 自动读取 test 目录下的 yaml 并启动依赖 -- 自动导入 test 目录下的 DB 初始化 SQL -- 提供特定容器内的 healthcheck (mysql, mc, redis) -- 提供一站式解决 UT 服务依赖的工具版本 (testcli) - -### 编译安装 -*使用本工具/库需要前置安装好 docker & docker-compose@v1.24.1^* - -#### Method 1. With go get -```shell -go get -u github.com/go-kratos/kratos/tool/testcli -$GOPATH/bin/testcli -h -``` -#### Method 2. Build with Go -```shell -cd github.com/go-kratos/kratos/tool/testcli -go build -o $GOPATH/bin/testcli -$GOPATH/bin/testcli -h -``` -#### Method 3. Import with Kratos pkg -```Go -import "github.com/go-kratos/kratos/pkg/testing/lich" -``` - -### 构建数据 -#### Step 1. create docker-compose.yml -创建依赖服务的 docker-compose.yml,并把它放在项目路径下的 test 文件夹下面。例如: -```shell -mkdir -p $YOUR_PROJECT/test -``` -```yaml -version: "3.7" - -services: - db: - image: mysql:5.6 - ports: - - 3306:3306 - environment: - - MYSQL_ROOT_PASSWORD=root - volumes: - - .:/docker-entrypoint-initdb.d - command: [ - '--character-set-server=utf8', - '--collation-server=utf8_unicode_ci' - ] - - redis: - image: redis - ports: - - 6379:6379 -``` -一般来讲,我们推荐在项目根目录创建 test 目录,里面存放描述服务的yml,以及需要初始化的数据(database.sql等)。 - -同时也需要注意,正确的对容器内服务进行健康检测,testcli会在容器的health状态执行UT,其实我们也内置了针对几个较为通用镜像(mysql mariadb mc redis)的健康检测,也就是不写也没事(^^;; - -#### Step 2. export database.sql -构造初始化的数据(database.sql等),当然也把它也在 test 文件夹里。 -```sql -CREATE DATABASE IF NOT EXISTS `YOUR_DATABASE_NAME`; - -SET NAMES 'utf8'; -USE `YOUR_DATABASE_NAME`; - -CREATE TABLE IF NOT EXISTS `YOUR_TABLE_NAME` ( - `id` int(11) unsigned NOT NULL AUTO_INCREMENT COMMENT '主键', - PRIMARY KEY (`id`), -) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='YOUR_TABLE_NAME'; -``` -这里需要注意,在创建库/表的时候尽量加上 IF NOT EXISTS,以给予一定程度的容错,以及 SET NAMES 'utf8'; 用于解决客户端连接乱码问题。 - -#### Step 3. change your project mysql config -```toml -[mysql] - addr = "127.0.0.1:3306" - dsn = "root:root@tcp(127.0.0.1:3306)/YOUR_DATABASE?timeout=1s&readTimeout=1s&writeTimeout=1s&parseTime=true&loc=Local&charset=utf8mb4,utf8" - active = 20 - idle = 10 - idleTimeout ="1s" - queryTimeout = "1s" - execTimeout = "1s" - tranTimeout = "1s" -``` -在 *Step 1* 我们已经指定了服务对外暴露的端口为3306(这当然也可以是你指定的任何值),那理所应当的我们也要修改项目连接数据库的配置~ - -Great! 至此你已经完成了运行所需要用到的数据配置,接下来就来运行它。 - -### 运行 -开头也说过本工具支持两种运行方式:testcli 二进制工具版本和 go package 源码包,业务方可以根据需求场景进行选择。 -#### Method 1. With testcli tool -*已支持的 flag: -f,--nodown,down,run* -- -f,指定 docker-compose.yaml 文件路径,默认为当前目录下。 -- --nodown,指定是否在UT执行完成后保留容器,以供下次复用。 -- down,teardown 销毁当前项目下这个 compose 文件产生的容器。 -- run,运行你当前语言的单测执行命令(如:golang为 go test -v ./) - -example: -```shell -testcli -f ../../test/docker-compose.yaml run go test -v ./ -``` -#### Method 2. Import with Kratos pkg -- Step1. 在 Dao|Service 层中的 TestMain 单测主入口中,import "github.com/go-kratos/kratos/pkg/testing/lich" 引入testcli工具的go库版本。 -- Step2. 使用 flag.Set("f", "../../test/docker-compose.yaml") 指定 docker-compose.yaml 文件的路径。 -- Step3. 在 flag.Parse() 后即可使用 lich.Setup() 安装依赖&初始化数据(注意测试用例执行结束后 lich.Teardown() 回收下~) -- Step4. 运行 `go test -v ./ `看看效果吧~ - -example: -```Go -package dao - - -import ( - "flag" - "os" - "strings" - "testing" - - "github.com/go-kratos/kratos/pkg/conf/paladin" - "github.com/go-kratos/kratos/pkg/testing/lich" - ) - -var ( - d *Dao -) - -func TestMain(m *testing.M) { - flag.Set("conf", "../../configs") - flag.Set("f", "../../test/docker-compose.yaml") - flag.Parse() - if err := paladin.Init(); err != nil { - panic(err) - } - if err := lich.Setup(); err != nil { - panic(err) - } - defer lich.Teardown() - d = New() - if code := m.Run(); code != 0 { - panic(code) - } -} - ``` -## 注意 -因为启动mysql容器较为缓慢,健康检测的机制会重试3次,每次暂留5秒钟,基本在10s内mysql就能从creating到服务正常启动! -当然你也可以在使用 testcli 时加上 --nodown,使其不用每次跑都新建容器,只在第一次跑的时候会初始化容器,后面都进行复用,这样速度会快很多。 - diff --git a/docs/ut-testgen.md b/docs/ut-testgen.md deleted file mode 100644 index c13f90a3a..000000000 --- a/docs/ut-testgen.md +++ /dev/null @@ -1,45 +0,0 @@ -## testgen UT代码自动生成器 -解放你的双手,让你的UT一步到位! - -### 功能和特性 -- 支持生成 Dao|Service 层UT代码功能(每个方法包含一个正向用例) -- 支持生成 Dao|Service 层测试入口文件dao_test.go, service_test.go(用于控制初始化,控制测试流程等) -- 支持生成Mock代码(使用GoMock框架) -- 支持选择不同模式生成不同代码(使用"–m mode"指定) -- 生成单元测试代码时,同时支持传入目录或文件 -- 支持指定方法追加生成测试用例(使用"–func funcName"指定) - -### 编译安装 -#### Method 1. With go get -```shell -go get -u github.com/go-kratos/kratos/tool/testgen -$GOPATH/bin/testgen -h -``` -#### Method 2. Build with Go -```shell -cd github.com/go-kratos/kratos/tool/testgen -go build -o $GOPATH/bin/testgen -$GOPATH/bin/testgen -h -``` -### 运行 -#### 生成Dao/Service层单元UT -```shell -$GOPATH/bin/testgen YOUR_PROJECT/dao # default mode -$GOPATH/bin/testgen --m test path/to/your/pkg -$GOPATH/bin/testgen --func functionName path/to/your/pkg -``` - -#### 生成接口类型 -```shell -$GOPATH/bin/testgen --m interface YOUR_PROJECT/dao #当前仅支持传目录,如目录包含子目录也会做处理 -``` - -#### 生成Mock代码 - ```shell -$GOPATH/bin/testgen --m mock YOUR_PROJECT/dao #仅传入包路径即可 -``` - -#### 生成Monkey代码 -```shell -$GOPATH/bin/testgen --m monkey yourCodeDirPath #仅传入包路径即可 -``` diff --git a/docs/ut.md b/docs/ut.md deleted file mode 100644 index 8907f0517..000000000 --- a/docs/ut.md +++ /dev/null @@ -1,32 +0,0 @@ -# 背景 -单元测试即对最小可测试单元进行检查和验证,它可以很好的让你的代码在上测试环境之前自己就能前置的发现问题,解决问题。当然每个语言都有原生支持的 UT 框架,不过在 Kratos 里面我们需要有一些配套设施以及周边工具来辅助我们构筑整个 UT 生态。 - -# 工具链 -- testgen UT代码自动生成器(README: tool/testgen/README.md) -- testcli UT运行环境构建工具(README: tool/testcli/README.md) - -# 测试框架选型 -golang 的单元测试,既可以用官方自带的 testing 包,也有开源的如 testify、goconvey 业内知名,使用非常多也很好用的框架。 - -根据一番调研和内部使用经验,我们确定: -> - testing 作为基础库测试框架(非常精简不过够用) -> - goconvey 作为业务程序的单元测试框架(因为涉及比较多的业务场景和流程控制判断,比如更丰富的res值判断、上下文嵌套支持、还有webUI等) - -# 单元测试标准 -1. 覆盖率,当前标准:60%(所有包均需达到) -尽量达到70%以上。当然覆盖率并不能完全说明单元测试的质量,开发者需要考虑关键的条件判断和预期的结果。复杂的代码是需要好好设计测试用例的。 -2. 通过率,当前标准:100%(所有用例中的断言必须通过) - -# 书写建议 -1. 结果验证 -> - 校验err是否为nil. err是go函数的标配了,也是最基础的判断,如果err不为nil,基本上函数返回值或者处理肯定是有问题了。 -> - 检验res值是否正确。res值的校验是非常重要的,也是很容易忽略的地方。比如返回结构体对象,要对结构体的成员进行判断,而有可能里面是0值。goconvey对res值的判断支持是非常友好的。 - -2. 逻辑验证 -> 业务代码经常是流程比较复杂的,而函数的执行结果也是有上下文的,比如有不同条件分支。goconvey就非常优雅的支持了这种情况,可以嵌套执行。单元测试要结合业务代码逻辑,才能尽量的减少线上bug。 - -3. 如何mock -主要分以下3块: -> - 基础组件,如mc、redis、mysql等,由 testcli(testing/lich) 起基础镜像支持(需要提供建表、INSERT语句)与本地开发环境一致,也保证了结果的一致性。 -> - rpc server,如 xxxx-service 需要定义 interface 供业务依赖方使用。所有rpc server 都必须要提供一个interface+mock代码(gomock)。 -> - http server则直接写mock代码gock。 diff --git a/docs/warden-balancer.md b/docs/warden-balancer.md deleted file mode 100644 index 5ffc9ee68..000000000 --- a/docs/warden-balancer.md +++ /dev/null @@ -1,39 +0,0 @@ -# Warden Balancer - -## 介绍 -grpc-go内置了round-robin轮询,但由于自带的轮询算法不支持权重,也不支持color筛选等需求,故需要重新实现一个负载均衡算法。 - -## WRR (Weighted Round Robin) -该算法在加权轮询法基础上增加了动态调节权重值,用户可以在为每一个节点先配置一个初始的权重分,之后算法会根据节点cpu、延迟、服务端错误率、客户端错误率动态打分,在将打分乘用户自定义的初始权重分得到最后的权重值。 - -## P2C (Pick of two choices) -本算法通过随机选择两个node选择优胜者来避免羊群效应,并通过ewma尽量获取服务端的实时状态。 - -服务端: -服务端获取最近500ms内的CPU使用率(需要将cgroup设置的限制考虑进去,并除于CPU核心数),并将CPU使用率乘与1000后塞入每次grpc请求中的的Trailer中夹带返回: -cpu_usage -uint64 encoded with string -cpu_usage : 1000 - -客户端: -主要参数: -* server_cpu:通过每次请求中服务端塞在trailer中的cpu_usage拿到服务端最近500ms内的cpu使用率 -* inflight:当前客户端正在发送并等待response的请求数(pending request) -* latency: 加权移动平均算法计算出的接口延迟 -* client_success:加权移动平均算法计算出的请求成功率(只记录grpc内部错误,比如context deadline) - -目前客户端,已经默认使用p2c负载均衡算法`grpc.WithBalancerName(p2c.Name)`: -```go -// NewClient returns a new blank Client instance with a default client interceptor. -// opt can be used to add grpc dial options. -func NewClient(conf *ClientConfig, opt ...grpc.DialOption) *Client { - c := new(Client) - if err := c.SetConfig(conf); err != nil { - panic(err) - } - c.UseOpt(grpc.WithBalancerName(p2c.Name)) - c.UseOpt(opt...) - c.Use(c.recovery(), clientLogging(), c.handle()) - return c -} -``` diff --git a/docs/warden-mid.md b/docs/warden-mid.md deleted file mode 100644 index 1fbd2ad89..000000000 --- a/docs/warden-mid.md +++ /dev/null @@ -1,374 +0,0 @@ -# 说明 - -gRPC暴露了两个拦截器接口,分别是: - -* `grpc.UnaryServerInterceptor`服务端拦截器 -* `grpc.UnaryClientInterceptor`客户端拦截器 - -基于两个拦截器可以针对性的定制公共模块的封装代码,比如`warden/logging.go`是通用日志逻辑。 - -# 分析 - -## 服务端拦截器 - -让我们先看一下`grpc.UnaryServerInterceptor`的声明,[官方代码位置](https://github.com/grpc/grpc-go/blob/master/interceptor.go): - -```go -// UnaryServerInfo consists of various information about a unary RPC on -// server side. All per-rpc information may be mutated by the interceptor. -type UnaryServerInfo struct { - // Server is the service implementation the user provides. This is read-only. - Server interface{} - // FullMethod is the full RPC method string, i.e., /package.service/method. - FullMethod string -} - -// UnaryHandler defines the handler invoked by UnaryServerInterceptor to complete the normal -// execution of a unary RPC. If a UnaryHandler returns an error, it should be produced by the -// status package, or else gRPC will use codes.Unknown as the status code and err.Error() as -// the status message of the RPC. -type UnaryHandler func(ctx context.Context, req interface{}) (interface{}, error) - -// UnaryServerInterceptor provides a hook to intercept the execution of a unary RPC on the server. info -// contains all the information of this RPC the interceptor can operate on. And handler is the wrapper -// of the service method implementation. It is the responsibility of the interceptor to invoke handler -// to complete the RPC. -type UnaryServerInterceptor func(ctx context.Context, req interface{}, info *UnaryServerInfo, handler UnaryHandler) (resp interface{}, err error) -``` - -看起来很简单包括: - -* 一个`UnaryServerInfo`结构体用于`Server`和`FullMethod`字段传递,`Server`为`gRPC server`的对象实例,`FullMethod`为请求方法的全名 -* 一个`UnaryHandler`方法用于传递`Handler`,就是基于`proto`文件`service`内声明而生成的方法 -* 一个`UnaryServerInterceptor`用于拦截`Handler`方法,可在`Handler`执行前后插入拦截代码 - -为了更形象的说明拦截器的执行过程,请看基于`proto`生成的以下代码[代码位置](https://github.com/go-kratos/kratos-demo/blob/master/api/api.pb.go): - -```go -func _Demo_SayHello_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { - in := new(HelloReq) - if err := dec(in); err != nil { - return nil, err - } - if interceptor == nil { - return srv.(DemoServer).SayHello(ctx, in) - } - info := &grpc.UnaryServerInfo{ - Server: srv, - FullMethod: "/demo.service.v1.Demo/SayHello", - } - handler := func(ctx context.Context, req interface{}) (interface{}, error) { - return srv.(DemoServer).SayHello(ctx, req.(*HelloReq)) - } - return interceptor(ctx, in, info, handler) -} -``` - -这个`_Demo_SayHello_Handler`方法是关键,该方法会被包装为`grpc.ServiceDesc`结构,被注册到gRPC内部,具体可在生成的`pb.go`代码内查找`s.RegisterService(&_Demo_serviceDesc, srv)`。 - -* 当`gRPC server`收到一次请求时,首先根据请求方法从注册到`server`内的`grpc.ServiceDesc`找到该方法对应的`Handler`如:`_Demo_SayHello_Handler`并执行 -* `_Demo_SayHello_Handler`执行过程请看上面具体代码,当`interceptor`不为`nil`时,会将`SayHello`包装为`grpc.UnaryHandler`结构传递给`interceptor` - -这样就完成了`UnaryServerInterceptor`的执行过程。那么`_Demo_SayHello_Handler`内的`interceptor`是如何注入到`gRPC server`内,则看下面这段代码[官方代码位置](https://github.com/grpc/grpc-go/blob/master/server.go): - -```go -// UnaryInterceptor returns a ServerOption that sets the UnaryServerInterceptor for the -// server. Only one unary interceptor can be installed. The construction of multiple -// interceptors (e.g., chaining) can be implemented at the caller. -func UnaryInterceptor(i UnaryServerInterceptor) ServerOption { - return func(o *options) { - if o.unaryInt != nil { - panic("The unary server interceptor was already set and may not be reset.") - } - o.unaryInt = i - } -} -``` - -请一定注意这方法的注释!!! - -> Only one unary interceptor can be installed. The construction of multiple interceptors (e.g., chaining) can be implemented at the caller. - -`gRPC`本身只支持一个`interceptor`,想要多`interceptors`需要自己实现~~所以`warden`基于`grpc.UnaryClientInterceptor`实现了`interceptor chain`,请看下面代码[代码位置](https://github.com/go-kratos/kratos/blob/master/pkg/net/rpc/warden/server.go): - -```go -// Use attachs a global inteceptor to the server. -// For example, this is the right place for a rate limiter or error management inteceptor. -func (s *Server) Use(handlers ...grpc.UnaryServerInterceptor) *Server { - finalSize := len(s.handlers) + len(handlers) - if finalSize >= int(_abortIndex) { - panic("warden: server use too many handlers") - } - mergedHandlers := make([]grpc.UnaryServerInterceptor, finalSize) - copy(mergedHandlers, s.handlers) - copy(mergedHandlers[len(s.handlers):], handlers) - s.handlers = mergedHandlers - return s -} - -// interceptor is a single interceptor out of a chain of many interceptors. -// Execution is done in left-to-right order, including passing of context. -// For example ChainUnaryServer(one, two, three) will execute one before two before three, and three -// will see context changes of one and two. -func (s *Server) interceptor(ctx context.Context, req interface{}, args *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (interface{}, error) { - var ( - i int - chain grpc.UnaryHandler - ) - - n := len(s.handlers) - if n == 0 { - return handler(ctx, req) - } - - chain = func(ic context.Context, ir interface{}) (interface{}, error) { - if i == n-1 { - return handler(ic, ir) - } - i++ - return s.handlers[i](ic, ir, args, chain) - } - - return s.handlers[0](ctx, req, args, chain) -} -``` - -很简单的逻辑: - -* `warden server`使用`Use`方法进行`grpc.UnaryServerInterceptor`的注入,而`func (s *Server) interceptor`本身就实现了`grpc.UnaryServerInterceptor` -* `func (s *Server) interceptor`可以根据注册的`grpc.UnaryServerInterceptor`顺序从前到后依次执行 - -而`warden`在初始化的时候将该方法本身注册到了`gRPC server`,在`NewServer`方法内可以看到下面代码: - -```go -opt = append(opt, keepParam, grpc.UnaryInterceptor(s.interceptor)) -s.server = grpc.NewServer(opt...) -``` - -如此完整的服务端拦截器逻辑就串联完成。 - -## 客户端拦截器 - - -让我们先看一下`grpc.UnaryClientInterceptor`的声明,[官方代码位置](https://github.com/grpc/grpc-go/blob/master/interceptor.go): - -```go -// UnaryInvoker is called by UnaryClientInterceptor to complete RPCs. -type UnaryInvoker func(ctx context.Context, method string, req, reply interface{}, cc *ClientConn, opts ...CallOption) error - -// UnaryClientInterceptor intercepts the execution of a unary RPC on the client. invoker is the handler to complete the RPC -// and it is the responsibility of the interceptor to call it. -// This is an EXPERIMENTAL API. -type UnaryClientInterceptor func(ctx context.Context, method string, req, reply interface{}, cc *ClientConn, invoker UnaryInvoker, opts ...CallOption) error -``` - -看起来和服务端拦截器并没有什么太大的区别,比较简单包括: - -* 一个`UnaryInvoker`表示客户端具体要发出的执行方法 -* 一个`UnaryClientInterceptor`用于拦截`Invoker`方法,可在`Invoker`执行前后插入拦截代码 - -具体执行过程,请看基于`proto`生成的下面代码[代码位置](https://github.com/go-kratos/kratos-demo/blob/master/api/api.pb.go): - -```go -func (c *demoClient) SayHello(ctx context.Context, in *HelloReq, opts ...grpc.CallOption) (*google_protobuf1.Empty, error) { - out := new(google_protobuf1.Empty) - err := grpc.Invoke(ctx, "/demo.service.v1.Demo/SayHello", in, out, c.cc, opts...) - if err != nil { - return nil, err - } - return out, nil -} -``` - -当客户端调用`SayHello`时可以看到执行了`grpc.Invoke`方法,并且将`fullMethod`和其他参数传入,最终会执行下面代码[官方代码位置](https://github.com/grpc/grpc-go/blob/master/call.go): - -```go -// Invoke sends the RPC request on the wire and returns after response is -// received. This is typically called by generated code. -// -// All errors returned by Invoke are compatible with the status package. -func (cc *ClientConn) Invoke(ctx context.Context, method string, args, reply interface{}, opts ...CallOption) error { - // allow interceptor to see all applicable call options, which means those - // configured as defaults from dial option as well as per-call options - opts = combine(cc.dopts.callOptions, opts) - - if cc.dopts.unaryInt != nil { - return cc.dopts.unaryInt(ctx, method, args, reply, cc, invoke, opts...) - } - return invoke(ctx, method, args, reply, cc, opts...) -} -``` - -其中的`unaryInt`即为客户端连接创建时注册的拦截器,使用下面代码注册[官方代码位置](https://github.com/grpc/grpc-go/blob/master/dialoptions.go): - -```go -// WithUnaryInterceptor returns a DialOption that specifies the interceptor for -// unary RPCs. -func WithUnaryInterceptor(f UnaryClientInterceptor) DialOption { - return newFuncDialOption(func(o *dialOptions) { - o.unaryInt = f - }) -} -``` - -需要注意的是客户端的拦截器在官方`gRPC`内也只能支持注册一个,与服务端拦截器`interceptor chain`逻辑类似`warden`在客户端拦截器也做了相同处理,并且在客户端连接时进行注册,请看下面代码[代码位置](https://github.com/go-kratos/kratos/blob/master/pkg/net/rpc/warden/client.go): - -```go -// Use attachs a global inteceptor to the Client. -// For example, this is the right place for a circuit breaker or error management inteceptor. -func (c *Client) Use(handlers ...grpc.UnaryClientInterceptor) *Client { - finalSize := len(c.handlers) + len(handlers) - if finalSize >= int(_abortIndex) { - panic("warden: client use too many handlers") - } - mergedHandlers := make([]grpc.UnaryClientInterceptor, finalSize) - copy(mergedHandlers, c.handlers) - copy(mergedHandlers[len(c.handlers):], handlers) - c.handlers = mergedHandlers - return c -} - -// chainUnaryClient creates a single interceptor out of a chain of many interceptors. -// -// Execution is done in left-to-right order, including passing of context. -// For example ChainUnaryClient(one, two, three) will execute one before two before three. -func (c *Client) chainUnaryClient() grpc.UnaryClientInterceptor { - n := len(c.handlers) - if n == 0 { - return func(ctx context.Context, method string, req, reply interface{}, - cc *grpc.ClientConn, invoker grpc.UnaryInvoker, opts ...grpc.CallOption) error { - return invoker(ctx, method, req, reply, cc, opts...) - } - } - - return func(ctx context.Context, method string, req, reply interface{}, - cc *grpc.ClientConn, invoker grpc.UnaryInvoker, opts ...grpc.CallOption) error { - var ( - i int - chainHandler grpc.UnaryInvoker - ) - chainHandler = func(ictx context.Context, imethod string, ireq, ireply interface{}, ic *grpc.ClientConn, iopts ...grpc.CallOption) error { - if i == n-1 { - return invoker(ictx, imethod, ireq, ireply, ic, iopts...) - } - i++ - return c.handlers[i](ictx, imethod, ireq, ireply, ic, chainHandler, iopts...) - } - - return c.handlers[0](ctx, method, req, reply, cc, chainHandler, opts...) - } -} -``` - -如此完整的客户端拦截器逻辑就串联完成。 - -# 实现自己的拦截器 - -以服务端拦截器`logging`为例: - -```go -// serverLogging warden grpc logging -func serverLogging() grpc.UnaryServerInterceptor { - return func(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (interface{}, error) { - // NOTE: handler执行之前的拦截代码:主要获取一些关键参数,如耗时计时、ip等 - // 如果自定义的拦截器只需要在handler执行后,那么可以直接执行handler - - startTime := time.Now() - caller := metadata.String(ctx, metadata.Caller) - if caller == "" { - caller = "no_user" - } - var remoteIP string - if peerInfo, ok := peer.FromContext(ctx); ok { - remoteIP = peerInfo.Addr.String() - } - var quota float64 - if deadline, ok := ctx.Deadline(); ok { - quota = time.Until(deadline).Seconds() - } - - // call server handler - resp, err := handler(ctx, req) // NOTE: 以具体执行的handler为分界线!!! - - // NOTE: handler执行之后的拦截代码:主要进行耗时计算、日志记录 - // 如果自定义的拦截器在handler执行后不需要逻辑,这可直接返回 - - // after server response - code := ecode.Cause(err).Code() - duration := time.Since(startTime) - - // monitor - statsServer.Timing(caller, int64(duration/time.Millisecond), info.FullMethod) - statsServer.Incr(caller, info.FullMethod, strconv.Itoa(code)) - logFields := []log.D{ - log.KVString("user", caller), - log.KVString("ip", remoteIP), - log.KVString("path", info.FullMethod), - log.KVInt("ret", code), - // TODO: it will panic if someone remove String method from protobuf message struct that auto generate from protoc. - log.KVString("args", req.(fmt.Stringer).String()), - log.KVFloat64("ts", duration.Seconds()), - log.KVFloat64("timeout_quota", quota), - log.KVString("source", "grpc-access-log"), - } - if err != nil { - logFields = append(logFields, log.KV("error", err.Error()), log.KV("stack", fmt.Sprintf("%+v", err))) - } - logFn(code, duration)(ctx, logFields...) - return resp, err - } -} -``` - -# 内置拦截器 - -## 自适应限流拦截器 - -更多关于自适应限流的信息,请参考:[kratos 自适应限流](ratelimit.md) - -```go -package grpc - -import ( - pb "kratos-demo/api" - "kratos-demo/internal/service" - "github.com/go-kratos/kratos/pkg/conf/paladin" - "github.com/go-kratos/kratos/pkg/net/rpc/warden" - "github.com/go-kratos/kratos/pkg/net/rpc/warden/ratelimiter" -) - -// New new a grpc server. -func New(svc *service.Service) *warden.Server { - var rc struct { - Server *warden.ServerConfig - } - if err := paladin.Get("grpc.toml").UnmarshalTOML(&rc); err != nil { - if err != paladin.ErrNotExist { - panic(err) - } - } - ws := warden.NewServer(rc.Server) - - // 挂载自适应限流拦截器到 warden server,使用默认配置 - limiter := ratelimiter.New(nil) - ws.Use(limiter.Limit()) - - // 注意替换这里: - // RegisterDemoServer方法是在"api"目录下代码生成的 - // 对应proto文件内自定义的service名字,请使用正确方法名替换 - pb.RegisterDemoServer(ws.Server(), svc) - - ws, err := ws.Start() - if err != nil { - panic(err) - } - return ws -} -``` - -# 扩展阅读 - -[warden快速开始](warden-quickstart.md) -[warden基于pb生成](warden-pb.md) -[warden负载均衡](warden-balancer.md) -[warden服务发现](warden-resolver.md) diff --git a/docs/warden-pb.md b/docs/warden-pb.md deleted file mode 100644 index a02cb1d04..000000000 --- a/docs/warden-pb.md +++ /dev/null @@ -1,48 +0,0 @@ -# 介绍 - -基于proto文件可以快速生成`warden`框架对应的代码,提前需要准备以下工作: - -* 安装`kratos tool protoc`工具,请看[kratos工具](kratos-tool.md) -* 编写`proto`文件,示例可参考[kratos-demo内proto文件](https://github.com/go-kratos/kratos-demo/blob/master/api/api.proto) - -### kratos工具说明 - -`kratos tool protoc`工具可以生成`warden` `bm` `swagger`对应的代码和文档,想要单独生成`warden`代码只需加上`--grpc`如: - -```shell -# generate gRPC -kratos tool protoc --grpc api.proto -``` - -# 使用 - -建议在项目`api`目录下编写`proto`文件及生成对应的代码,可参考[kratos-demo内的api目录](https://github.com/go-kratos/kratos-demo/tree/master/api)。 - -执行命令后生成的`api.pb.go`代码,注意其中的`DemoClient`和`DemoServer`,其中: - -* `DemoClient`接口为客户端调用接口,相对应的有`demoClient`结构体为其实现 -* `DemoServer`接口为服务端接口声明,需要业务自己实现该接口的所有方法,`kratos`建议在`internal/service`目录下使用`Service`结构体实现 - -`internal/service`内的`Service`结构实现了`DemoServer`接口可参考[kratos-demo内的service](https://github.com/go-kratos/kratos-demo/blob/master/internal/service/service.go)内的如下代码: - -```go -// SayHelloURL bm demo func. -func (s *Service) SayHelloURL(ctx context.Context, req *pb.HelloReq) (reply *pb.HelloResp, err error) { - reply = &pb.HelloResp{ - Content: "hello " + req.Name, - } - fmt.Printf("hello url %s", req.Name) - return -} -``` - -更详细的客户端和服务端使用请看[warden快速开始](warden-quickstart.md) - -# 扩展阅读 - -[warden快速开始](warden-quickstart.md) -[warden拦截器](warden-mid.md) -[warden负载均衡](warden-balancer.md) -[warden服务发现](warden-resolver.md) - - diff --git a/docs/warden-quickstart.md b/docs/warden-quickstart.md deleted file mode 100644 index 6173ebd55..000000000 --- a/docs/warden-quickstart.md +++ /dev/null @@ -1,171 +0,0 @@ -# 准备工作 - -推荐使用[kratos工具](kratos-tool.md)快速生成带`grpc`的项目,如我们生成一个叫`kratos-demo`的项目。 - -``` -kratos new kratos-demo --proto -``` - -# pb文件 - -创建项目成功后,进入`api`目录下可以看到`api.proto`和`api.pb.go`和`generate.go`文件,其中: - -* `api.proto`是gRPC server的描述文件 -* `api.pb.go`是基于`api.proto`生成的代码文件 -* `generate.go`是用于`kratos tool protoc`执行`go generate`进行代码生成的临时文件 - -接下来可以将以上三个文件全部删除或者保留`generate.go`,之后编写自己的proto文件,确认proto无误后,进行代码生成: - -* 可直接执行`kratos tool protoc`,该命令会调用protoc工具生成`.pb.go`文件 -* 如`generate.go`没删除,也可以执行`go generate`命令,将调用`kratos tool protoc`工具进行代码生成 - -[kratos工具请看](kratos-tool.md) - -### 如没看kprotoc文档,请看下面这段话 - -`kratos tool protoc`用于快速生成`pb.go`文件,但目前windows和Linux需要先自己安装`protoc`工具,具体请看[protoc说明](protoc.md)。 - -# 注册server - -进入`internal/server/grpc`目录打开`server.go`文件,可以看到以下代码,只需要替换以下注释内容就可以启动一个gRPC服务。 - -```go -package grpc - -import ( - pb "kratos-demo/api" - "kratos-demo/internal/service" - "github.com/go-kratos/kratos/pkg/conf/paladin" - "github.com/go-kratos/kratos/pkg/net/rpc/warden" -) - -// New new a grpc server. -func New(svc *service.Service) *warden.Server { - var rc struct { - Server *warden.ServerConfig - } - if err := paladin.Get("grpc.toml").UnmarshalTOML(&rc); err != nil { - if err != paladin.ErrNotExist { - panic(err) - } - } - ws := warden.NewServer(rc.Server) - // 注意替换这里: - // RegisterDemoServer方法是在"api"目录下代码生成的 - // 对应proto文件内自定义的service名字,请使用正确方法名替换 - pb.RegisterDemoServer(ws.Server(), svc) - ws, err := ws.Start() - if err != nil { - panic(err) - } - return ws -} -``` - -### 注册注意 - -```go -// SayHello grpc demo func. -func (s *Service) SayHello(ctx context.Context, req *pb.HelloReq) (reply *empty.Empty, err error) { - reply = new(empty.Empty) - fmt.Printf("hello %s", req.Name) - return -} -``` - -请进入`internal/service`内找到`SayHello`方法,注意方法的入参和出参,都是按照gRPC的方法声明对应的: - -* 第一个参数必须是`context.Context`,第二个必须是proto内定义的`message`对应生成的结构体 -* 第一个返回值必须是proto内定义的`message`对应生成的结构体,第二个参数必须是`error` -* 在http框架bm中,如果共用proto文件生成bm代码,那么也可以直接使用该service方法 - -建议service严格按照此格式声明方法使其能够在bm和warden内共用。 - -# client调用 - -请进入`internal/dao`方法内,一般对资源的处理都会在这一层封装。 -对于`client`端,前提必须有对应`proto`文件生成的代码,那么有两种选择: - -* 拷贝proto文件到自己项目下并且执行代码生成 -* 直接import服务端的api package - -> 这也是业务代码我们加了一层`internal`的关系,服务对外暴露的只有接口 - -不管哪一种方式,以下初始化gRPC client的代码建议伴随生成的代码存放在统一目录下: - -```go -package dao - -import ( - "context" - - "github.com/go-kratos/kratos/pkg/net/rpc/warden" - - "google.golang.org/grpc" -) - -// target server addrs. -const target = "direct://default/127.0.0.1:9000,127.0.0.1:9091" // NOTE: example - -// NewClient new member grpc client -func NewClient(cfg *warden.ClientConfig, opts ...grpc.DialOption) (DemoClient, error) { - client := warden.NewClient(cfg, opts...) - conn, err := client.Dial(context.Background(), target) - if err != nil { - return nil, err - } - // 注意替换这里: - // NewDemoClient方法是在"api"目录下代码生成的 - // 对应proto文件内自定义的service名字,请使用正确方法名替换 - return NewDemoClient(conn), nil -} -``` - -其中,`target`为gRPC用于服务发现的目标,使用标准url资源格式提供给resolver用于服务发现。`warden`默认使用`direct`直连方式,直接与`server`端进行连接。如果在使用其他服务发现组件请看[warden服务发现](warden-resolver.md)。 - -有了初始化`Client`的代码,我们的`Dao`对象即可进行初始化和使用,以下以直接import服务端api包为例: - -```go -package dao - -import( - demoapi "kratos-demo/api" - grpcempty "github.com/golang/protobuf/ptypes/empty" - "github.com/go-kratos/kratos/pkg/net/rpc/warden" - - "github.com/pkg/errors" -) - -type Dao struct{ - demoClient demoapi.DemoClient -} - -// New account dao. -func New() (d *Dao) { - cfg := &warden.ClientConfig{} - paladin.Get("grpc.toml").UnmarshalTOML(cfg) - d = &Dao{} - var err error - if d.demoClient, err = demoapi.NewClient(cfg); err != nil { - panic(err) - } - return -} - -// SayHello say hello. -func (d *Dao) SayHello(c context.Context, req *demoapi.HelloReq) (resp *grpcempty.Empty, err error) { - if resp, err = d.demoClient.SayHello(c, req); err != nil { - err = errors.Wrapf(err, "%v", arg) - } - return -} -``` - -如此在`internal/service`层就可以进行资源的方法调用。 - -# 扩展阅读 - -[warden拦截器](warden-mid.md) -[warden基于pb生成](warden-pb.md) -[warden服务发现](warden-resolver.md) -[warden负载均衡](warden-balancer.md) diff --git a/docs/warden-resolver.md b/docs/warden-resolver.md deleted file mode 100644 index 38232743f..000000000 --- a/docs/warden-resolver.md +++ /dev/null @@ -1,254 +0,0 @@ -# 前提 - -服务注册与发现最简单的就是`direct`固定服务端地址的直连方式。也就是服务端正常监听端口启动不进行额外操作,客户端使用如下`target`: - -```url -direct://default/127.0.0.1:9000,127.0.0.1:9091 -``` - -> `target`就是标准的`URL`资源定位符[查看WIKI](https://zh.wikipedia.org/wiki/%E7%BB%9F%E4%B8%80%E8%B5%84%E6%BA%90%E5%AE%9A%E4%BD%8D%E7%AC%A6) - -其中`direct`为协议类型,此处表示直接使用该`URL`内提供的地址`127.0.0.1:9000,127.0.0.1:9091`进行连接,而`default`在此处无意义仅当做占位符。 - -# gRPC Resolver - -gRPC暴露了服务发现的接口`resolver.Builder`和`resolver.ClientConn`和`resolver.Resolver`,[官方代码位置](https://github.com/grpc/grpc-go/blob/master/resolver/resolver.go): - -```go -// Builder creates a resolver that will be used to watch name resolution updates. -type Builder interface { - // Build creates a new resolver for the given target. - // - // gRPC dial calls Build synchronously, and fails if the returned error is - // not nil. - Build(target Target, cc ClientConn, opts BuildOption) (Resolver, error) - // Scheme returns the scheme supported by this resolver. - // Scheme is defined at https://github.com/grpc/grpc/blob/master/doc/naming.md. - Scheme() string -} - -// ClientConn contains the callbacks for resolver to notify any updates -// to the gRPC ClientConn. -// -// This interface is to be implemented by gRPC. Users should not need a -// brand new implementation of this interface. For the situations like -// testing, the new implementation should embed this interface. This allows -// gRPC to add new methods to this interface. -type ClientConn interface { - // UpdateState updates the state of the ClientConn appropriately. - UpdateState(State) - // NewAddress is called by resolver to notify ClientConn a new list - // of resolved addresses. - // The address list should be the complete list of resolved addresses. - // - // Deprecated: Use UpdateState instead. - NewAddress(addresses []Address) - // NewServiceConfig is called by resolver to notify ClientConn a new - // service config. The service config should be provided as a json string. - // - // Deprecated: Use UpdateState instead. - NewServiceConfig(serviceConfig string) -} - -// Resolver watches for the updates on the specified target. -// Updates include address updates and service config updates. -type Resolver interface { - // ResolveNow will be called by gRPC to try to resolve the target name - // again. It's just a hint, resolver can ignore this if it's not necessary. - // - // It could be called multiple times concurrently. - ResolveNow(ResolveNowOption) - // Close closes the resolver. - Close() -} -``` - -下面依次分析这三个接口的作用: - -* `Builder`用于gRPC内部创建`Resolver`接口的实现,但注意声明的`Build`方法将接口`ClientConn`作为参数传入了 -* `ClientConn`接口有两个废弃方法不用管,看`UpdateState`方法需要传入`State`结构,看代码可以发现其中包含了`Addresses []Address // Resolved addresses for the target`,可以看出是需要将服务发现得到的`Address`对象列表告诉`ClientConn`的对象 -* `Resolver`提供了`ResolveNow`用于被gRPC尝试重新进行服务发现 - -看完这三个接口就可以明白gRPC的服务发现实现逻辑,通过`Builder`进行`Reslover`的创建,在`Build`的过程中将服务发现的地址信息丢给`ClientConn`用于内部连接创建等逻辑。主要逻辑可以按下面顺序来看源码理解: - -* 当`client`在`Dial`时会根据`target`解析的`scheme`获取对应的`Builder`,[官方代码位置](https://github.com/grpc/grpc-go/blob/master/clientconn.go#L242) -* 当`Dial`成功会创建出结构体`ClientConn`的对象[官方代码位置](https://github.com/grpc/grpc-go/blob/master/clientconn.go#L447)(注意不是上面的`ClientConn`接口),可以看到结构体`ClientConn`内的成员`resolverWrapper`又实现了接口`ClientConn`的方法[官方代码位置](https://github.com/grpc/grpc-go/blob/master/resolver_conn_wrapper.go) -* 当`resolverWrapper`被初始化时就会调用`Build`方法[官方代码位置](https://github.com/grpc/grpc-go/blob/master/resolver_conn_wrapper.go#L89),其中参数为接口`ClientConn`传入的是`ccResolverWrapper` -* 当用户基于`Builder`的实现进行`UpdateState`调用时,则会触发结构体`ClientConn`的`updateResolverState`方法[官方代码位置](https://github.com/grpc/grpc-go/blob/master/resolver_conn_wrapper.go#L109),`updateResolverState`则会对传入的`Address`进行初始化等逻辑[官方代码位置](https://github.com/grpc/grpc-go/blob/master/clientconn.go#L553) - -如此整个服务发现过程就结束了。从中也可以看出gRPC官方提供的三个接口还是很灵活的,但也正因为灵活要实现稍微麻烦一些,而`Address`[官方代码位置](https://github.com/grpc/grpc-go/blob/master/resolver/resolver.go#L79)如果直接被业务拿来用于服务节点信息的描述结构则显得有些过于简单。 - -所以`warden`包装了gRPC的整个服务发现实现逻辑,代码分别位于`pkg/naming/naming.go`和`warden/resolver/resolver.go`,其中: - -* `naming.go`内定义了用于描述业务实例的`Instance`结构、用于服务注册的`Registry`接口、用于服务发现的`Resolver`接口 -* `resolver.go`内实现了gRPC官方的`resolver.Builder`和`resolver.Resolver`接口,但也暴露了`naming.go`内的`naming.Builder`和`naming.Resolver`接口 - -# warden Resolver - -接下来看`naming`内的接口如下: - -```go -// Resolver resolve naming service -type Resolver interface { - Fetch(context.Context) (*InstancesInfo, bool) - Watch() <-chan struct{} - Close() error -} - -// Builder resolver builder. -type Builder interface { - Build(id string) Resolver - Scheme() string -} -``` - -可以看到封装方式与gRPC官方的方法一样,通过`Builder`进行`Resolver`的初始化。不同的是通过封装将参数进行了简化: - -* `Build`只需要传对应的服务`id`即可:`warden/resolver/resolver.go`在gRPC进行调用后,会根据`Scheme`方法查询对应的`naming.Builder`实现并调用`Build`将`id`传入,而`naming.Resolver`的实现即可通过`id`去对应的服务发现中间件进行实例信息的查询 -* 而`Resolver`则对方法进行了扩展,除了简单进行`Fetch`操作外还多了`Watch`方法,用于监听服务发现中间件的节点变化情况,从而能够实时的进行服务实例信息的更新 - -在`naming/discovery`内实现了基于[discovery](https://github.com/bilibili/discovery)为中间件的服务注册与发现逻辑。如果要实现其他中间件如`etcd`|`zookeeper`等的逻辑,参考`naming/discovery/discovery.go`内的逻辑,将与`discovery`的交互逻辑替换掉即可(后续会默认将etcd/zk等实现,敬请期待)。 - -# 使用discovery - -因为`warden`内默认使用`direct`的方式,所以要使用[discovery](https://github.com/bilibili/discovery)需要在业务的`NewClient`前进行注册,代码如下: - -```go -package dao - -import ( - "context" - - "github.com/go-kratos/kratos/pkg/naming/discovery" - "github.com/go-kratos/kratos/pkg/net/rpc/warden" - "github.com/go-kratos/kratos/pkg/net/rpc/warden/resolver" - - "google.golang.org/grpc" -) - -// AppID your appid, ensure unique. -const AppID = "demo.service" // NOTE: example - -func init(){ - // NOTE: 注意这段代码,表示要使用discovery进行服务发现 - // NOTE: 还需注意的是,resolver.Register是全局生效的,所以建议该代码放在进程初始化的时候执行 - // NOTE: !!!切记不要在一个进程内进行多个不同中间件的Register!!! - // NOTE: 在启动应用时,可以通过flag(-discovery.nodes) 或者 环境配置(DISCOVERY_NODES)指定discovery节点 - resolver.Register(discovery.Builder()) -} - -// NewClient new member grpc client -func NewClient(cfg *warden.ClientConfig, opts ...grpc.DialOption) (DemoClient, error) { - client := warden.NewClient(cfg, opts...) - conn, err := client.Dial(context.Background(), "discovery://default/"+AppID) - if err != nil { - return nil, err - } - // 注意替换这里: - // NewDemoClient方法是在"api"目录下代码生成的 - // 对应proto文件内自定义的service名字,请使用正确方法名替换 - return NewDemoClient(conn), nil -} -``` - -> 注意:`resolver.Register`是全局行为,建议放在包加载阶段或main方法开始时执行,该方法执行后会在gRPC内注册构造方法 - -`target`是`discovery://default/${appid}`,当gRPC内进行解析后会得到`scheme`=`discovery`和`appid`,然后进行以下逻辑: - -1. `warden/resolver.Builder`会通过`scheme`获取到`naming/discovery.Builder`对象(靠`resolver.Register`注册过的) -2. 拿到`naming/discovery.Builder`后执行`Build(appid)`构造`naming/discovery.Discovery` -3. `naming/discovery.Discovery`对象基于`appid`就知道要获取哪个服务的实例信息 - -# 服务注册 - -客户端既然使用了[discovery](https://github.com/bilibili/discovery)进行服务发现,也就意味着服务端启动后必须将自己注册给[discovery](https://github.com/bilibili/discovery)知道。 - -相对服务发现来讲,服务注册则简单很多,看`naming/discovery/discovery.go`内的代码实现了`naming/naming.go`内的`Registry`接口,服务端启动时可以参考下面代码进行注册: - -```go -// 该代码可放在main.go,当warden server进行初始化之后 -// 省略... - -ip := "" // NOTE: 必须拿到您实例节点的真实IP, -port := "" // NOTE: 必须拿到您实例grpc监听的真实端口,warden默认监听9000 -hn, _ := os.Hostname() -dis := discovery.New(nil) -ins := &naming.Instance{ - Zone: env.Zone, - Env: env.DeployEnv, - AppID: "your app id", - Hostname: hn, - Addrs: []string{ - "grpc://" + ip + ":" + port, - }, -} -cancel, err := dis.Register(context.Background(), ins) -if err != nil { - panic(err) -} - -// 省略... - -// 特别注意!!! -// cancel必须在进程退出时执行!!! -cancel() -``` - - - -# 使用ETCD - -和使用discovery类似,只需要在注册时使用etcd naming即可。 - -```go -package dao - -import ( - "context" - - "github.com/go-kratos/kratos/pkg/naming/etcd" - "github.com/go-kratos/kratos/pkg/net/rpc/warden" - "github.com/go-kratos/kratos/pkg/net/rpc/warden/resolver" - - "google.golang.org/grpc" -) - -// AppID your appid, ensure unique. -const AppID = "demo.service" // NOTE: example - -func init(){ - // NOTE: 注意这段代码,表示要使用etcd进行服务发现 ,其他事项参考discovery的说明 - // NOTE: 在启动应用时,可以通过flag(-etcd.endpoints) 或者 环境配置(ETCD_ENDPOINTS)指定etcd节点 - // NOTE: 如果需要自己指定配置时 需要同时设置DialTimeout 与 DialOptions: []grpc.DialOption{grpc.WithBlock()} - resolver.Register(etcd.Builder(nil)) -} - -// NewClient new member grpc client -func NewClient(cfg *warden.ClientConfig, opts ...grpc.DialOption) (DemoClient, error) { - client := warden.NewClient(cfg, opts...) - // 这里使用etcd scheme - conn, err := client.Dial(context.Background(), "etcd://default/"+AppID) - if err != nil { - return nil, err - } - // 注意替换这里: - // NewDemoClient方法是在"api"目录下代码生成的 - // 对应proto文件内自定义的service名字,请使用正确方法名替换 - return NewDemoClient(conn), nil -} -``` - -etcd的服务注册与discovery基本相同,可以传入详细的etcd配置项, 或者传入nil后通过flag(-etcd.endpoints)/环境配置(ETCD_ENDPOINTS)来指定etcd节点。 - -### 其他配置项 - -etcd默认的全局keyPrefix为kratos_etcd,当该keyPrefix与项目中其他keyPrefix冲突时可以通过flag(-etcd.prefix)或者环境配置(ETCD_PREFIX)来指定keyPrefix。 - - - -# 扩展阅读 - -[warden快速开始](warden-quickstart.md) -[warden拦截器](warden-mid.md) -[warden基于pb生成](warden-pb.md) -[warden负载均衡](warden-balancer.md) diff --git a/docs/warden.md b/docs/warden.md deleted file mode 100644 index 996a0c81d..000000000 --- a/docs/warden.md +++ /dev/null @@ -1,41 +0,0 @@ -# 背景 - -我们需要统一的rpc服务,经过选型讨论决定直接使用成熟的、跨语言的gRPC。 - -# 概览 - -* 不改gRPC源码,基于接口进行包装集成trace、log、prom等组件 -* 打通自有服务注册发现系统[discovery](https://github.com/bilibili/discovery) -* 实现更平滑可靠的负载均衡算法 - -# 拦截器 - -gRPC暴露了两个拦截器接口,分别是: - -* `grpc.UnaryServerInterceptor`服务端拦截器 -* `grpc.UnaryClientInterceptor`客户端拦截器 - -基于两个拦截器可以针对性的定制公共模块的封装代码,比如`warden/logging.go`是通用日志逻辑。 - -[warden拦截器](warden-mid.md) - -# 服务发现 - -`warden`默认使用`direct`方式直连,正常线上都会使用第三方服务注册与发现中间件,`warden`内包含了[discovery](https://github.com/bilibili/discovery)的逻辑实现,想使用如`etcd`、`zookeeper`等也可以,都请看下面文档。 - -[warden服务发现](warden-resolver.md) - -# 负载均衡 - -实现了`wrr`和`p2c`两种算法,默认使用`p2c`。 - -[warden负载均衡](warden-balancer.md) - -# 扩展阅读 - -- [warden快速开始](warden-quickstart.md) -- [warden拦截器](warden-mid.md) -- [warden负载均衡](warden-balancer.md) -- [warden基于pb生成](warden-pb.md) -- [warden服务发现](warden-resolver.md) - diff --git a/encoding/encoding.go b/encoding/encoding.go new file mode 100644 index 000000000..0e9141a7a --- /dev/null +++ b/encoding/encoding.go @@ -0,0 +1,40 @@ +package encoding + +import "strings" + +// Codec defines the interface Transport uses to encode and decode messages. Note +// that implementations of this interface must be thread safe; a Codec's +// methods can be called from concurrent goroutines. +type Codec interface { + // Marshal returns the wire format of v. + Marshal(v interface{}) ([]byte, error) + // Unmarshal parses the wire format into v. + Unmarshal(data []byte, v interface{}) error + // Name returns the name of the Codec implementation. The returned string + // will be used as part of content type in transmission. The result must be + // static; the result cannot change between calls. + Name() string +} + +var registeredCodecs = make(map[string]Codec) + +// RegisterCodec registers the provided Codec for use with all Transport clients and +// servers. +func RegisterCodec(codec Codec) { + if codec == nil { + panic("cannot register a nil Codec") + } + if codec.Name() == "" { + panic("cannot register Codec with empty string result for Name()") + } + contentSubtype := strings.ToLower(codec.Name()) + registeredCodecs[contentSubtype] = codec +} + +// GetCodec gets a registered Codec by content-subtype, or nil if no Codec is +// registered for the content-subtype. +// +// The content-subtype is expected to be lowercase. +func GetCodec(contentSubtype string) Codec { + return registeredCodecs[contentSubtype] +} diff --git a/encoding/json/json.go b/encoding/json/json.go new file mode 100644 index 000000000..8c88f4ceb --- /dev/null +++ b/encoding/json/json.go @@ -0,0 +1,49 @@ +package json + +import ( + "encoding/json" + + "github.com/go-kratos/kratos/v2/encoding" + + "google.golang.org/protobuf/encoding/protojson" + "google.golang.org/protobuf/proto" +) + +// Name is the name registered for the json codec. +const Name = "json" + +var ( + // MarshalOptions is a configurable JSON format marshaler. + MarshalOptions = protojson.MarshalOptions{ + EmitUnpopulated: true, + } + // UnmarshalOptions is a configurable JSON format parser. + UnmarshalOptions = protojson.UnmarshalOptions{ + DiscardUnknown: true, + } +) + +func init() { + encoding.RegisterCodec(codec{}) +} + +// codec is a Codec implementation with json. +type codec struct{} + +func (codec) Marshal(v interface{}) ([]byte, error) { + if m, ok := v.(proto.Message); ok { + return MarshalOptions.Marshal(m) + } + return json.Marshal(v) +} + +func (codec) Unmarshal(data []byte, v interface{}) error { + if m, ok := v.(proto.Message); ok { + return UnmarshalOptions.Unmarshal(data, m) + } + return json.Unmarshal(data, v) +} + +func (codec) Name() string { + return Name +} diff --git a/encoding/proto/proto.go b/encoding/proto/proto.go new file mode 100644 index 000000000..26a291441 --- /dev/null +++ b/encoding/proto/proto.go @@ -0,0 +1,30 @@ +// Package proto defines the protobuf codec. Importing this package will +// register the codec. +package proto + +import ( + "google.golang.org/grpc/encoding" + "google.golang.org/protobuf/proto" +) + +// Name is the name registered for the proto compressor. +const Name = "proto" + +func init() { + encoding.RegisterCodec(codec{}) +} + +// codec is a Codec implementation with protobuf. It is the default codec for Transport. +type codec struct{} + +func (codec) Marshal(v interface{}) ([]byte, error) { + return proto.Marshal(v.(proto.Message)) +} + +func (codec) Unmarshal(data []byte, v interface{}) error { + return proto.Unmarshal(data, v.(proto.Message)) +} + +func (codec) Name() string { + return Name +} diff --git a/errors/codes.go b/errors/codes.go new file mode 100644 index 000000000..acd1c4a71 --- /dev/null +++ b/errors/codes.go @@ -0,0 +1,285 @@ +package errors + +import ( + "errors" + "fmt" +) + +// Cancelled The operation was cancelled, typically by the caller. +// HTTP Mapping: 499 Client Closed Request +func Cancelled(reason, format string, a ...interface{}) error { + return &StatusError{ + Code: 1, + Reason: reason, + Message: fmt.Sprintf(format, a...), + } +} + +func IsCancelled(err error) bool { + if se := new(StatusError); errors.As(err, &se) { + return se.Code == 1 + } + return false +} + +// Unknown error. +// HTTP Mapping: 500 Internal Server Error +func Unknown(reason, format string, a ...interface{}) error { + return &StatusError{ + Code: 2, + Reason: reason, + Message: fmt.Sprintf(format, a...), + } +} + +func IsUnknown(err error) bool { + if se := new(StatusError); errors.As(err, &se) { + return se.Code == 2 + } + return false +} + +// InvalidArgument The client specified an invalid argument. +// HTTP Mapping: 400 Bad Request +func InvalidArgument(reason, format string, a ...interface{}) error { + return &StatusError{ + Code: 3, + Reason: reason, + Message: fmt.Sprintf(format, a...), + } +} + +func IsInvalidArgument(err error) bool { + if se := new(StatusError); errors.As(err, &se) { + return se.Code == 3 + } + return false +} + +// DeadlineExceeded The deadline expired before the operation could complete. +// HTTP Mapping: 504 Gateway Timeout +func DeadlineExceeded(reason, format string, a ...interface{}) error { + return &StatusError{ + Code: 4, + Reason: reason, + Message: fmt.Sprintf(format, a...), + } +} + +func IsDeadlineExceeded(err error) bool { + if se := new(StatusError); errors.As(err, &se) { + return se.Code == 4 + } + return false +} + +// NotFound Some requested entity (e.g., file or directory) was not found. +// HTTP Mapping: 404 Not Found +func NotFound(reason, format string, a ...interface{}) error { + return &StatusError{ + Code: 5, + Reason: reason, + Message: fmt.Sprintf(format, a...), + } +} + +func IsNotFound(err error) bool { + if se := new(StatusError); errors.As(err, &se) { + return se.Code == 5 + } + return false +} + +// AlreadyExists The entity that a client attempted to create (e.g., file or directory) already exists. +// HTTP Mapping: 409 Conflict +func AlreadyExists(reason, format string, a ...interface{}) error { + return &StatusError{ + Code: 6, + Reason: reason, + Message: fmt.Sprintf(format, a...), + } +} + +func IsAlreadyExists(err error) bool { + if se := new(StatusError); errors.As(err, &se) { + return se.Code == 6 + } + return false +} + +// PermissionDenied The caller does not have permission to execute the specified operation. +// HTTP Mapping: 403 Forbidden +func PermissionDenied(reason, format string, a ...interface{}) error { + return &StatusError{ + Code: 7, + Reason: reason, + Message: fmt.Sprintf(format, a...), + } +} + +func IsPermissionDenied(err error) bool { + if se := new(StatusError); errors.As(err, &se) { + return se.Code == 7 + } + return false +} + +// ResourceExhausted Some resource has been exhausted, perhaps a per-user quota, or +// perhaps the entire file system is out of space. +// HTTP Mapping: 429 Too Many Requests +func ResourceExhausted(reason, format string, a ...interface{}) error { + return &StatusError{ + Code: 8, + Reason: reason, + Message: fmt.Sprintf(format, a...), + } +} + +func IsResourceExhausted(err error) bool { + if se := new(StatusError); errors.As(err, &se) { + return se.Code == 8 + } + return false +} + +// FailedPrecondition The operation was rejected because the system is not in a state +// required for the operation's execution. +// HTTP Mapping: 400 Bad Request +func FailedPrecondition(reason, format string, a ...interface{}) error { + return &StatusError{ + Code: 9, + Reason: reason, + Message: fmt.Sprintf(format, a...), + } +} + +func IsFailedPrecondition(err error) bool { + if se := new(StatusError); errors.As(err, &se) { + return se.Code == 9 + } + return false +} + +// Aborted The operation was aborted, typically due to a concurrency issue such as +// a sequencer check failure or transaction abort. +// HTTP Mapping: 409 Conflict +func Aborted(reason, format string, a ...interface{}) error { + return &StatusError{ + Code: 10, + Reason: reason, + Message: fmt.Sprintf(format, a...), + } +} + +func IsAborted(err error) bool { + if se := new(StatusError); errors.As(err, &se) { + return se.Code == 10 + } + return false +} + +// OutOfRange The operation was attempted past the valid range. E.g., seeking or +// reading past end-of-file. +// HTTP Mapping: 400 Bad Request +func OutOfRange(reason, format string, a ...interface{}) error { + return &StatusError{ + Code: 11, + Reason: reason, + Message: fmt.Sprintf(format, a...), + } +} + +func IsOutOfRange(err error) bool { + if se := new(StatusError); errors.As(err, &se) { + return se.Code == 11 + } + return false +} + +// Unimplemented The operation is not implemented or is not supported/enabled in this service. +// HTTP Mapping: 501 Not Implemented +func Unimplemented(reason, format string, a ...interface{}) error { + return &StatusError{ + Code: 12, + Reason: reason, + Message: fmt.Sprintf(format, a...), + } +} + +func IsUnimplemented(err error) bool { + if se := new(StatusError); errors.As(err, &se) { + return se.Code == 12 + } + return false +} + +// Internal This means that some invariants expected by the +// underlying system have been broken. This error code is reserved +// for serious errors. +// +// HTTP Mapping: 500 Internal Server Error +func Internal(reason, format string, a ...interface{}) error { + return &StatusError{ + Code: 13, + Reason: reason, + Message: fmt.Sprintf(format, a...), + } +} + +func IsInternal(err error) bool { + if se := new(StatusError); errors.As(err, &se) { + return se.Code == 13 + } + return false +} + +// Unavailable The service is currently unavailable. +// HTTP Mapping: 503 Service Unavailable +func Unavailable(reason, format string, a ...interface{}) error { + return &StatusError{ + Code: 14, + Reason: reason, + Message: fmt.Sprintf(format, a...), + } +} + +func IsUnavailable(err error) bool { + if se := new(StatusError); errors.As(err, &se) { + return se.Code == 14 + } + return false +} + +// DataLoss Unrecoverable data loss or corruption. +// HTTP Mapping: 500 Internal Server Error +func DataLoss(reason, format string, a ...interface{}) error { + return &StatusError{ + Code: 15, + Reason: reason, + Message: fmt.Sprintf(format, a...), + } +} + +func IsDataLoss(err error) bool { + if se := new(StatusError); errors.As(err, &se) { + return se.Code == 15 + } + return false +} + +// Unauthorized The request does not have valid authentication credentials for the operation. +// HTTP Mapping: 401 Unauthorized +func Unauthorized(reason, format string, a ...interface{}) error { + return &StatusError{ + Code: 16, + Reason: reason, + Message: fmt.Sprintf(format, a...), + } +} + +func IsUnauthorized(err error) bool { + if se := new(StatusError); errors.As(err, &se) { + return se.Code == 16 + } + return false +} diff --git a/errors/errors.go b/errors/errors.go new file mode 100644 index 000000000..69f7b8a64 --- /dev/null +++ b/errors/errors.go @@ -0,0 +1,73 @@ +package errors + +import ( + "errors" + "fmt" +) + +const ( + // UnknownReason is unknown reason for error info. + UnknownReason = "" + // SupportPackageIsVersion1 this constant should not be referenced by any other code. + SupportPackageIsVersion1 = true +) + +var _ error = (*StatusError)(nil) + +// StatusError contains an error response from the server. +type StatusError = Status + +// Is matches each error in the chain with the target value. +func (e *StatusError) Is(target error) bool { + err, ok := target.(*StatusError) + if ok { + return e.Code == err.Code + } + return false +} + +func (e *StatusError) Error() string { + return fmt.Sprintf("error: code = %d reason = %s message = %s details = %+v", e.Code, e.Reason, e.Message, e.Details) +} + +// Error returns a Status representing c and msg. +func Error(code int32, reason, message string) error { + return &StatusError{ + Code: code, + Reason: reason, + Message: message, + } +} + +// Errorf returns New(c, fmt.Sprintf(format, a...)). +func Errorf(code int32, reason, format string, a ...interface{}) error { + return Error(code, reason, fmt.Sprintf(format, a...)) +} + +// Code returns the status code. +func Code(err error) int32 { + if err == nil { + return 0 // ok + } + if se := new(StatusError); errors.As(err, &se) { + return se.Code + } + return 2 // unknown +} + +// Reason returns the status for a particular error. +// It supports wrapped errors. +func Reason(err error) string { + if se := new(StatusError); errors.As(err, &se) { + return se.Reason + } + return UnknownReason +} + +// FromError returns status error. +func FromError(err error) (*StatusError, bool) { + if se := new(StatusError); errors.As(err, &se) { + return se, true + } + return nil, false +} diff --git a/errors/errors.pb.go b/errors/errors.pb.go new file mode 100644 index 000000000..3b142ba34 --- /dev/null +++ b/errors/errors.pb.go @@ -0,0 +1,187 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.25.0 +// protoc v3.13.0 +// source: errors.proto + +package errors + +import ( + proto "github.com/golang/protobuf/proto" + any "github.com/golang/protobuf/ptypes/any" + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" + reflect "reflect" + sync "sync" +) + +const ( + // Verify that this generated code is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) + // Verify that runtime/protoimpl is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) +) + +// This is a compile-time assertion that a sufficiently up-to-date version +// of the legacy proto package is being used. +const _ = proto.ProtoPackageIsVersion4 + +type Status struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Code int32 `protobuf:"varint,1,opt,name=code,proto3" json:"code,omitempty"` + Reason string `protobuf:"bytes,2,opt,name=reason,proto3" json:"reason,omitempty"` + Message string `protobuf:"bytes,3,opt,name=message,proto3" json:"message,omitempty"` + Details []*any.Any `protobuf:"bytes,4,rep,name=details,proto3" json:"details,omitempty"` +} + +func (x *Status) Reset() { + *x = Status{} + if protoimpl.UnsafeEnabled { + mi := &file_errors_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *Status) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Status) ProtoMessage() {} + +func (x *Status) ProtoReflect() protoreflect.Message { + mi := &file_errors_proto_msgTypes[0] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use Status.ProtoReflect.Descriptor instead. +func (*Status) Descriptor() ([]byte, []int) { + return file_errors_proto_rawDescGZIP(), []int{0} +} + +func (x *Status) GetCode() int32 { + if x != nil { + return x.Code + } + return 0 +} + +func (x *Status) GetReason() string { + if x != nil { + return x.Reason + } + return "" +} + +func (x *Status) GetMessage() string { + if x != nil { + return x.Message + } + return "" +} + +func (x *Status) GetDetails() []*any.Any { + if x != nil { + return x.Details + } + return nil +} + +var File_errors_proto protoreflect.FileDescriptor + +var file_errors_proto_rawDesc = []byte{ + 0x0a, 0x0c, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x73, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x0d, + 0x6b, 0x72, 0x61, 0x74, 0x6f, 0x73, 0x2e, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x73, 0x1a, 0x19, 0x67, + 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2f, 0x61, + 0x6e, 0x79, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0x7e, 0x0a, 0x06, 0x53, 0x74, 0x61, 0x74, + 0x75, 0x73, 0x12, 0x12, 0x0a, 0x04, 0x63, 0x6f, 0x64, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x05, + 0x52, 0x04, 0x63, 0x6f, 0x64, 0x65, 0x12, 0x16, 0x0a, 0x06, 0x72, 0x65, 0x61, 0x73, 0x6f, 0x6e, + 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x72, 0x65, 0x61, 0x73, 0x6f, 0x6e, 0x12, 0x18, + 0x0a, 0x07, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, + 0x07, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x12, 0x2e, 0x0a, 0x07, 0x64, 0x65, 0x74, 0x61, + 0x69, 0x6c, 0x73, 0x18, 0x04, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x14, 0x2e, 0x67, 0x6f, 0x6f, 0x67, + 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x41, 0x6e, 0x79, 0x52, + 0x07, 0x64, 0x65, 0x74, 0x61, 0x69, 0x6c, 0x73, 0x42, 0x62, 0x0a, 0x11, 0x64, 0x65, 0x76, 0x2e, + 0x6b, 0x72, 0x61, 0x74, 0x6f, 0x73, 0x2e, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x73, 0x42, 0x0b, 0x45, + 0x72, 0x72, 0x6f, 0x72, 0x73, 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x50, 0x01, 0x5a, 0x2c, 0x67, 0x69, + 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x67, 0x6f, 0x2d, 0x6b, 0x72, 0x61, 0x74, + 0x6f, 0x73, 0x2f, 0x6b, 0x72, 0x61, 0x74, 0x6f, 0x73, 0x2f, 0x76, 0x32, 0x2f, 0x65, 0x72, 0x72, + 0x6f, 0x72, 0x73, 0x3b, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x73, 0xf8, 0x01, 0x01, 0xa2, 0x02, 0x0c, + 0x4b, 0x72, 0x61, 0x74, 0x6f, 0x73, 0x45, 0x72, 0x72, 0x6f, 0x72, 0x73, 0x62, 0x06, 0x70, 0x72, + 0x6f, 0x74, 0x6f, 0x33, +} + +var ( + file_errors_proto_rawDescOnce sync.Once + file_errors_proto_rawDescData = file_errors_proto_rawDesc +) + +func file_errors_proto_rawDescGZIP() []byte { + file_errors_proto_rawDescOnce.Do(func() { + file_errors_proto_rawDescData = protoimpl.X.CompressGZIP(file_errors_proto_rawDescData) + }) + return file_errors_proto_rawDescData +} + +var file_errors_proto_msgTypes = make([]protoimpl.MessageInfo, 1) +var file_errors_proto_goTypes = []interface{}{ + (*Status)(nil), // 0: kratos.errors.Status + (*any.Any)(nil), // 1: google.protobuf.Any +} +var file_errors_proto_depIdxs = []int32{ + 1, // 0: kratos.errors.Status.details:type_name -> google.protobuf.Any + 1, // [1:1] is the sub-list for method output_type + 1, // [1:1] is the sub-list for method input_type + 1, // [1:1] is the sub-list for extension type_name + 1, // [1:1] is the sub-list for extension extendee + 0, // [0:1] is the sub-list for field type_name +} + +func init() { file_errors_proto_init() } +func file_errors_proto_init() { + if File_errors_proto != nil { + return + } + if !protoimpl.UnsafeEnabled { + file_errors_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*Status); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + } + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: file_errors_proto_rawDesc, + NumEnums: 0, + NumMessages: 1, + NumExtensions: 0, + NumServices: 0, + }, + GoTypes: file_errors_proto_goTypes, + DependencyIndexes: file_errors_proto_depIdxs, + MessageInfos: file_errors_proto_msgTypes, + }.Build() + File_errors_proto = out.File + file_errors_proto_rawDesc = nil + file_errors_proto_goTypes = nil + file_errors_proto_depIdxs = nil +} diff --git a/errors/errors.proto b/errors/errors.proto new file mode 100644 index 000000000..653bbf961 --- /dev/null +++ b/errors/errors.proto @@ -0,0 +1,19 @@ +syntax = "proto3"; + +package kratos.errors; + +import "google/protobuf/any.proto"; + +option cc_enable_arenas = true; +option go_package = "github.com/go-kratos/kratos/v2/errors;errors"; +option java_multiple_files = true; +option java_outer_classname = "ErrorsProto"; +option java_package = "com.github.kratos.errors"; +option objc_class_prefix = "KratosErrors"; + +message Status { + int32 code = 1; + string reason = 2; + string message = 3; + repeated google.protobuf.Any details = 4; +} diff --git a/errors/errors_test.go b/errors/errors_test.go new file mode 100644 index 000000000..4c66b2934 --- /dev/null +++ b/errors/errors_test.go @@ -0,0 +1,54 @@ +package errors + +import ( + "errors" + "fmt" + "testing" +) + +func TestErrorsMatch(t *testing.T) { + s := &StatusError{Code: 1} + st := &StatusError{Code: 2} + + if errors.Is(s, st) { + t.Errorf("error is not match: %+v -> %+v", s, st) + } + + s.Code = 1 + st.Code = 1 + if !errors.Is(s, st) { + t.Errorf("error is not match: %+v -> %+v", s, st) + } + + s.Reason = "test_reason" + s.Reason = "test_reason" + + if !errors.Is(s, st) { + t.Errorf("error is not match: %+v -> %+v", s, st) + } + + if Reason(s) != "test_reason" { + t.Errorf("error is not match: %+v -> %+v", s, st) + } +} + +func TestErrorIs(t *testing.T) { + err1 := &StatusError{Code: 1} + t.Log(err1) + err2 := fmt.Errorf("wrap : %w", err1) + t.Log(err2) + + if !(errors.Is(err2, err1)) { + t.Errorf("error is not match: a: %v b: %v ", err2, err1) + } +} + +func TestErrorAs(t *testing.T) { + err1 := &StatusError{Code: 1} + err2 := fmt.Errorf("wrap : %w", err1) + + err3 := new(StatusError) + if !errors.As(err2, &err3) { + t.Errorf("error is not match: %v", err2) + } +} diff --git a/example/blademaster/middleware/auth/README.md b/example/blademaster/middleware/auth/README.md deleted file mode 100644 index 22f7554eb..000000000 --- a/example/blademaster/middleware/auth/README.md +++ /dev/null @@ -1,7 +0,0 @@ -#### blademaster/middleware/auth - -##### 项目简介 - -blademaster 的 authorization middleware,主要用于设置路由的认证策略 - -注:仅仅是个demo,请根据自身业务实现具体鉴权逻辑 diff --git a/example/blademaster/middleware/auth/auth.go b/example/blademaster/middleware/auth/auth.go deleted file mode 100644 index cb3accf66..000000000 --- a/example/blademaster/middleware/auth/auth.go +++ /dev/null @@ -1,153 +0,0 @@ -package auth - -import ( - "github.com/go-kratos/kratos/pkg/ecode" - bm "github.com/go-kratos/kratos/pkg/net/http/blademaster" - "github.com/go-kratos/kratos/pkg/net/metadata" -) - -// Config is the identify config model. -type Config struct { - // csrf switch. - DisableCSRF bool -} - -// Auth is the authorization middleware -type Auth struct { - conf *Config -} - -// authFunc will return mid and error by given context -type authFunc func(*bm.Context) (int64, error) - -var _defaultConf = &Config{ - DisableCSRF: false, -} - -// New is used to create an authorization middleware -func New(conf *Config) *Auth { - if conf == nil { - conf = _defaultConf - } - auth := &Auth{ - conf: conf, - } - return auth -} - -// User is used to mark path as access required. -// If `access_token` is exist in request form, it will using mobile access policy. -// Otherwise to web access policy. -func (a *Auth) User(ctx *bm.Context) { - req := ctx.Request - if req.Form.Get("access_token") == "" { - a.UserWeb(ctx) - return - } - a.UserMobile(ctx) -} - -// UserWeb is used to mark path as web access required. -func (a *Auth) UserWeb(ctx *bm.Context) { - a.midAuth(ctx, a.authCookie) -} - -// UserMobile is used to mark path as mobile access required. -func (a *Auth) UserMobile(ctx *bm.Context) { - a.midAuth(ctx, a.authToken) -} - -// Guest is used to mark path as guest policy. -// If `access_token` is exist in request form, it will using mobile access policy. -// Otherwise to web access policy. -func (a *Auth) Guest(ctx *bm.Context) { - req := ctx.Request - if req.Form.Get("access_token") == "" { - a.GuestWeb(ctx) - return - } - a.GuestMobile(ctx) -} - -// GuestWeb is used to mark path as web guest policy. -func (a *Auth) GuestWeb(ctx *bm.Context) { - a.guestAuth(ctx, a.authCookie) -} - -// GuestMobile is used to mark path as mobile guest policy. -func (a *Auth) GuestMobile(ctx *bm.Context) { - a.guestAuth(ctx, a.authToken) -} - -// authToken is used to authorize request by token -func (a *Auth) authToken(ctx *bm.Context) (int64, error) { - req := ctx.Request - key := req.Form.Get("access_token") - if key == "" { - return 0, ecode.Unauthorized - } - // NOTE: 请求登录鉴权服务接口,拿到对应的用户id - var mid int64 - // TODO: get mid from some code - return mid, nil -} - -// authCookie is used to authorize request by cookie -func (a *Auth) authCookie(ctx *bm.Context) (int64, error) { - req := ctx.Request - session, _ := req.Cookie("SESSION") - if session == nil { - return 0, ecode.Unauthorized - } - // NOTE: 请求登录鉴权服务接口,拿到对应的用户id - var mid int64 - // TODO: get mid from some code - - // check csrf - clientCsrf := req.FormValue("csrf") - if a.conf != nil && !a.conf.DisableCSRF && req.Method == "POST" { - // NOTE: 如果开启了CSRF认证,请从CSRF服务获取该用户关联的csrf - var csrf string // TODO: get csrf from some code - if clientCsrf != csrf { - return 0, ecode.Unauthorized - } - } - - return mid, nil -} - -func (a *Auth) midAuth(ctx *bm.Context, auth authFunc) { - mid, err := auth(ctx) - if err != nil { - ctx.JSON(nil, err) - ctx.Abort() - return - } - setMid(ctx, mid) -} - -func (a *Auth) guestAuth(ctx *bm.Context, auth authFunc) { - mid, err := auth(ctx) - // no error happened and mid is valid - if err == nil && mid > 0 { - setMid(ctx, mid) - return - } - - ec := ecode.Cause(err) - if ecode.Equal(ec, ecode.Unauthorized) { - ctx.JSON(nil, ec) - ctx.Abort() - return - } -} - -// set mid into context -// NOTE: This method is not thread safe. -func setMid(ctx *bm.Context, mid int64) { - ctx.Set(metadata.Mid, mid) - if md, ok := metadata.FromContext(ctx); ok { - md[metadata.Mid] = mid - return - } -} diff --git a/example/blademaster/middleware/auth/example_test.go b/example/blademaster/middleware/auth/example_test.go deleted file mode 100644 index d77d34494..000000000 --- a/example/blademaster/middleware/auth/example_test.go +++ /dev/null @@ -1,40 +0,0 @@ -package auth_test - -import ( - "fmt" - - "github.com/go-kratos/kratos/example/blademaster/middleware/auth" - bm "github.com/go-kratos/kratos/pkg/net/http/blademaster" - "github.com/go-kratos/kratos/pkg/net/metadata" -) - -// This example create a identify middleware instance and attach to several path, -// it will validate request by specified policy and put extra information into context. e.g., `mid`. -// It provides additional handler functions to provide the identification for your business handler. -func Example() { - myHandler := func(ctx *bm.Context) { - mid := metadata.Int64(ctx, metadata.Mid) - ctx.JSON(fmt.Sprintf("%d", mid), nil) - } - - authn := auth.New(&auth.Config{ - DisableCSRF: false, - }) - - e := bm.DefaultServer(nil) - - // mark `/user` path as User policy - e.GET("/user", authn.User, myHandler) - // mark `/mobile` path as UserMobile policy - e.GET("/mobile", authn.UserMobile, myHandler) - // mark `/web` path as UserWeb policy - e.GET("/web", authn.UserWeb, myHandler) - // mark `/guest` path as Guest policy - e.GET("/guest", authn.Guest, myHandler) - - o := e.Group("/owner", authn.User) - o.GET("/info", myHandler) - o.POST("/modify", myHandler) - - e.Start() -} diff --git a/example/protobuf/api.bm.go b/example/protobuf/api.bm.go deleted file mode 100644 index ebe6252ec..000000000 --- a/example/protobuf/api.bm.go +++ /dev/null @@ -1,54 +0,0 @@ -// Code generated by protoc-gen-bm v0.1, DO NOT EDIT. -// source: api.proto - -package api - -import ( - "context" - - bm "github.com/go-kratos/kratos/pkg/net/http/blademaster" - "github.com/go-kratos/kratos/pkg/net/http/blademaster/binding" -) -import google_protobuf1 "github.com/golang/protobuf/ptypes/empty" - -// to suppressed 'imported but not used warning' -var _ *bm.Context -var _ context.Context -var _ binding.StructValidator - -var PathUserInfo = "/user.api.User/Info" -var PathUserCard = "/user.api.User/Card" - -// UserBMServer is the server API for User service. -type UserBMServer interface { - Info(ctx context.Context, req *UserReq) (resp *InfoReply, err error) - - Card(ctx context.Context, req *UserReq) (resp *google_protobuf1.Empty, err error) -} - -var UserSvc UserBMServer - -func userInfo(c *bm.Context) { - p := new(UserReq) - if err := c.BindWith(p, binding.Default(c.Request.Method, c.Request.Header.Get("Content-Type"))); err != nil { - return - } - resp, err := UserSvc.Info(c, p) - c.JSON(resp, err) -} - -func userCard(c *bm.Context) { - p := new(UserReq) - if err := c.BindWith(p, binding.Default(c.Request.Method, c.Request.Header.Get("Content-Type"))); err != nil { - return - } - resp, err := UserSvc.Card(c, p) - c.JSON(resp, err) -} - -// RegisterUserBMServer Register the blademaster route -func RegisterUserBMServer(e *bm.Engine, server UserBMServer) { - UserSvc = server - e.GET("/user.api.User/Info", userInfo) - e.GET("/user.api.User/Card", userCard) -} diff --git a/example/protobuf/api.ecode.go b/example/protobuf/api.ecode.go deleted file mode 100644 index 7563f36ab..000000000 --- a/example/protobuf/api.ecode.go +++ /dev/null @@ -1,17 +0,0 @@ -// Code generated by protoc-gen-ecode v0.1, DO NOT EDIT. -// source: api.proto - -package api - -import ( - "github.com/go-kratos/kratos/pkg/ecode" -) - -// to suppressed 'imported but not used warning' -var _ ecode.Codes - -// UserErrCode ecode -var ( - UserNotExist = ecode.New(-404) - UserUpdateNameFailed = ecode.New(10000) -) diff --git a/example/protobuf/api.pb.go b/example/protobuf/api.pb.go deleted file mode 100644 index 3cdf09b00..000000000 --- a/example/protobuf/api.pb.go +++ /dev/null @@ -1,1000 +0,0 @@ -// Code generated by protoc-gen-gogo. DO NOT EDIT. -// source: api.proto - -package api - -import ( - context "context" - fmt "fmt" - _ "github.com/gogo/protobuf/gogoproto" - proto "github.com/golang/protobuf/proto" - grpc "google.golang.org/grpc" - io "io" - math "math" -) - -// Reference imports to suppress errors if they are not otherwise used. -var _ = proto.Marshal -var _ = fmt.Errorf -var _ = math.Inf - -// This is a compile-time assertion to ensure that this generated file -// is compatible with the proto package it is being compiled against. -// A compilation error at this line likely means your copy of the -// proto package needs to be updated. -const _ = proto.ProtoPackageIsVersion2 // please upgrade the proto package - -type UserErrCode int32 - -const ( - UserErrCode_OK UserErrCode = 0 - UserErrCode_UserNotExist UserErrCode = -404 - UserErrCode_UserUpdateNameFailed UserErrCode = 10000 -) - -var UserErrCode_name = map[int32]string{ - 0: "OK", - -404: "UserNotExist", - 10000: "UserUpdateNameFailed", -} - -var UserErrCode_value = map[string]int32{ - "OK": 0, - "UserNotExist": -404, - "UserUpdateNameFailed": 10000, -} - -func (x UserErrCode) String() string { - return proto.EnumName(UserErrCode_name, int32(x)) -} - -func (UserErrCode) EnumDescriptor() ([]byte, []int) { - return fileDescriptor_00212fb1f9d3bf1c, []int{0} -} - -type Info struct { - Mid int64 `protobuf:"varint,1,opt,name=mid,proto3" json:"mid"` - Name string `protobuf:"bytes,2,opt,name=name,proto3" json:"name"` - Sex string `protobuf:"bytes,3,opt,name=sex,proto3" json:"sex"` - Face string `protobuf:"bytes,4,opt,name=face,proto3" json:"face"` - Sign string `protobuf:"bytes,5,opt,name=sign,proto3" json:"sign"` - XXX_NoUnkeyedLiteral struct{} `json:"-"` - XXX_unrecognized []byte `json:"-"` - XXX_sizecache int32 `json:"-"` -} - -func (m *Info) Reset() { *m = Info{} } -func (m *Info) String() string { return proto.CompactTextString(m) } -func (*Info) ProtoMessage() {} -func (*Info) Descriptor() ([]byte, []int) { - return fileDescriptor_00212fb1f9d3bf1c, []int{0} -} -func (m *Info) XXX_Unmarshal(b []byte) error { - return m.Unmarshal(b) -} -func (m *Info) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { - if deterministic { - return xxx_messageInfo_Info.Marshal(b, m, deterministic) - } else { - b = b[:cap(b)] - n, err := m.MarshalTo(b) - if err != nil { - return nil, err - } - return b[:n], nil - } -} -func (m *Info) XXX_Merge(src proto.Message) { - xxx_messageInfo_Info.Merge(m, src) -} -func (m *Info) XXX_Size() int { - return m.Size() -} -func (m *Info) XXX_DiscardUnknown() { - xxx_messageInfo_Info.DiscardUnknown(m) -} - -var xxx_messageInfo_Info proto.InternalMessageInfo - -func (m *Info) GetMid() int64 { - if m != nil { - return m.Mid - } - return 0 -} - -func (m *Info) GetName() string { - if m != nil { - return m.Name - } - return "" -} - -func (m *Info) GetSex() string { - if m != nil { - return m.Sex - } - return "" -} - -func (m *Info) GetFace() string { - if m != nil { - return m.Face - } - return "" -} - -func (m *Info) GetSign() string { - if m != nil { - return m.Sign - } - return "" -} - -type UserReq struct { - Mid int64 `protobuf:"varint,1,opt,name=mid,proto3" json:"mid,omitempty" validate:"gt=0,required"` - XXX_NoUnkeyedLiteral struct{} `json:"-"` - XXX_unrecognized []byte `json:"-"` - XXX_sizecache int32 `json:"-"` -} - -func (m *UserReq) Reset() { *m = UserReq{} } -func (m *UserReq) String() string { return proto.CompactTextString(m) } -func (*UserReq) ProtoMessage() {} -func (*UserReq) Descriptor() ([]byte, []int) { - return fileDescriptor_00212fb1f9d3bf1c, []int{1} -} -func (m *UserReq) XXX_Unmarshal(b []byte) error { - return m.Unmarshal(b) -} -func (m *UserReq) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { - if deterministic { - return xxx_messageInfo_UserReq.Marshal(b, m, deterministic) - } else { - b = b[:cap(b)] - n, err := m.MarshalTo(b) - if err != nil { - return nil, err - } - return b[:n], nil - } -} -func (m *UserReq) XXX_Merge(src proto.Message) { - xxx_messageInfo_UserReq.Merge(m, src) -} -func (m *UserReq) XXX_Size() int { - return m.Size() -} -func (m *UserReq) XXX_DiscardUnknown() { - xxx_messageInfo_UserReq.DiscardUnknown(m) -} - -var xxx_messageInfo_UserReq proto.InternalMessageInfo - -func (m *UserReq) GetMid() int64 { - if m != nil { - return m.Mid - } - return 0 -} - -type InfoReply struct { - Info *Info `protobuf:"bytes,1,opt,name=info,proto3" json:"info,omitempty"` - XXX_NoUnkeyedLiteral struct{} `json:"-"` - XXX_unrecognized []byte `json:"-"` - XXX_sizecache int32 `json:"-"` -} - -func (m *InfoReply) Reset() { *m = InfoReply{} } -func (m *InfoReply) String() string { return proto.CompactTextString(m) } -func (*InfoReply) ProtoMessage() {} -func (*InfoReply) Descriptor() ([]byte, []int) { - return fileDescriptor_00212fb1f9d3bf1c, []int{2} -} -func (m *InfoReply) XXX_Unmarshal(b []byte) error { - return m.Unmarshal(b) -} -func (m *InfoReply) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { - if deterministic { - return xxx_messageInfo_InfoReply.Marshal(b, m, deterministic) - } else { - b = b[:cap(b)] - n, err := m.MarshalTo(b) - if err != nil { - return nil, err - } - return b[:n], nil - } -} -func (m *InfoReply) XXX_Merge(src proto.Message) { - xxx_messageInfo_InfoReply.Merge(m, src) -} -func (m *InfoReply) XXX_Size() int { - return m.Size() -} -func (m *InfoReply) XXX_DiscardUnknown() { - xxx_messageInfo_InfoReply.DiscardUnknown(m) -} - -var xxx_messageInfo_InfoReply proto.InternalMessageInfo - -func (m *InfoReply) GetInfo() *Info { - if m != nil { - return m.Info - } - return nil -} - -func init() { - proto.RegisterEnum("user.api.UserErrCode", UserErrCode_name, UserErrCode_value) - proto.RegisterType((*Info)(nil), "user.api.Info") - proto.RegisterType((*UserReq)(nil), "user.api.UserReq") - proto.RegisterType((*InfoReply)(nil), "user.api.InfoReply") -} - -func init() { proto.RegisterFile("api.proto", fileDescriptor_00212fb1f9d3bf1c) } - -var fileDescriptor_00212fb1f9d3bf1c = []byte{ - // 366 bytes of a gzipped FileDescriptorProto - 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0x5c, 0x51, 0x4d, 0x4b, 0xeb, 0x40, - 0x14, 0xed, 0x34, 0x79, 0xfd, 0x98, 0x3e, 0x1e, 0x7d, 0xf3, 0x9e, 0x90, 0x96, 0x92, 0x96, 0xac, - 0x8a, 0x68, 0x2a, 0x15, 0x04, 0x05, 0x37, 0x95, 0x0a, 0x52, 0xa8, 0x10, 0xe8, 0xc6, 0xdd, 0xb4, - 0x99, 0xc4, 0x81, 0x26, 0x93, 0xe6, 0x43, 0xea, 0xbf, 0x70, 0x25, 0xfe, 0x24, 0x97, 0xfe, 0x82, - 0x22, 0x75, 0x57, 0x5c, 0xb9, 0x16, 0x94, 0x7b, 0x5b, 0xa9, 0x38, 0x8b, 0xc3, 0x9c, 0x39, 0xf7, - 0x70, 0xcf, 0xbd, 0x43, 0xcb, 0x3c, 0x92, 0x76, 0x14, 0xab, 0x54, 0xb1, 0x52, 0x96, 0x88, 0xd8, - 0xe6, 0x91, 0xac, 0xef, 0xfb, 0x32, 0xbd, 0xce, 0xc6, 0xf6, 0x44, 0x05, 0x1d, 0x5f, 0xf9, 0xaa, - 0x83, 0x05, 0xe3, 0xcc, 0x43, 0x86, 0x04, 0x6f, 0x6b, 0xa3, 0x75, 0x4f, 0xa8, 0x7e, 0x11, 0x7a, - 0x8a, 0xd5, 0xa8, 0x16, 0x48, 0xd7, 0x20, 0x2d, 0xd2, 0xd6, 0x7a, 0xc5, 0xd5, 0xa2, 0x09, 0xd4, - 0x01, 0x60, 0x0d, 0xaa, 0x87, 0x3c, 0x10, 0x46, 0xbe, 0x45, 0xda, 0xe5, 0x5e, 0x69, 0xb5, 0x68, - 0x22, 0x77, 0x10, 0xc1, 0x98, 0x88, 0xb9, 0xa1, 0xa1, 0x88, 0xc6, 0x44, 0xcc, 0x1d, 0x00, 0x30, - 0x7a, 0x7c, 0x22, 0x0c, 0x7d, 0x6b, 0x04, 0xee, 0x20, 0x82, 0x9a, 0x48, 0x3f, 0x34, 0x7e, 0x6d, - 0x55, 0xe0, 0x0e, 0xa2, 0x75, 0x4c, 0x8b, 0xa3, 0x44, 0xc4, 0x8e, 0x98, 0x31, 0xfb, 0x7b, 0xb4, - 0xc6, 0xdb, 0xa2, 0x69, 0xdc, 0xf0, 0xa9, 0x74, 0x79, 0x2a, 0x4e, 0x2c, 0x3f, 0x3d, 0x3d, 0xd8, - 0x8b, 0xc5, 0x2c, 0x93, 0xb1, 0x70, 0x2d, 0xcc, 0x6b, 0x75, 0x68, 0x19, 0x46, 0x72, 0x44, 0x34, - 0xbd, 0x65, 0x16, 0xd5, 0x65, 0xe8, 0x29, 0x74, 0x57, 0xba, 0x7f, 0xec, 0xaf, 0x45, 0xd9, 0x58, - 0x82, 0xda, 0xee, 0x80, 0x56, 0xa0, 0x57, 0x3f, 0x8e, 0xcf, 0x94, 0x2b, 0x58, 0x81, 0xe6, 0x2f, - 0x07, 0xd5, 0x1c, 0xab, 0xd1, 0xdf, 0xf0, 0x3c, 0x54, 0x69, 0x7f, 0x2e, 0x93, 0xb4, 0xfa, 0xfa, - 0xfe, 0xb1, 0x3e, 0x84, 0xd5, 0xe8, 0x7f, 0x90, 0x46, 0x11, 0xa4, 0x18, 0xf2, 0x40, 0x9c, 0x73, - 0x39, 0x15, 0x6e, 0xf5, 0x6e, 0xd8, 0x3d, 0xa2, 0x3a, 0x48, 0xcc, 0xde, 0x2c, 0xf6, 0xef, 0xb6, - 0xe5, 0x66, 0xa0, 0xfa, 0xbf, 0x1f, 0x29, 0x20, 0x68, 0x6f, 0xe7, 0x71, 0x69, 0x92, 0xa7, 0xa5, - 0x49, 0x9e, 0x97, 0x26, 0x79, 0x78, 0x31, 0x73, 0x57, 0x1a, 0x8f, 0xe4, 0xb8, 0x80, 0xff, 0x74, - 0xf8, 0x19, 0x00, 0x00, 0xff, 0xff, 0x9d, 0xa7, 0xb7, 0x22, 0xed, 0x01, 0x00, 0x00, -} - -// Reference imports to suppress errors if they are not otherwise used. -var _ context.Context -var _ grpc.ClientConn - -// This is a compile-time assertion to ensure that this generated file -// is compatible with the grpc package it is being compiled against. -const _ = grpc.SupportPackageIsVersion4 - -// UserClient is the client API for User service. -// -// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://godoc.org/google.golang.org/grpc#ClientConn.NewStream. -type UserClient interface { - Info(ctx context.Context, in *UserReq, opts ...grpc.CallOption) (*InfoReply, error) -} - -type userClient struct { - cc *grpc.ClientConn -} - -func NewUserClient(cc *grpc.ClientConn) UserClient { - return &userClient{cc} -} - -func (c *userClient) Info(ctx context.Context, in *UserReq, opts ...grpc.CallOption) (*InfoReply, error) { - out := new(InfoReply) - err := c.cc.Invoke(ctx, "/user.api.User/Info", in, out, opts...) - if err != nil { - return nil, err - } - return out, nil -} - -// UserServer is the server API for User service. -type UserServer interface { - Info(context.Context, *UserReq) (*InfoReply, error) -} - -func RegisterUserServer(s *grpc.Server, srv UserServer) { - s.RegisterService(&_User_serviceDesc, srv) -} - -func _User_Info_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { - in := new(UserReq) - if err := dec(in); err != nil { - return nil, err - } - if interceptor == nil { - return srv.(UserServer).Info(ctx, in) - } - info := &grpc.UnaryServerInfo{ - Server: srv, - FullMethod: "/user.api.User/Info", - } - handler := func(ctx context.Context, req interface{}) (interface{}, error) { - return srv.(UserServer).Info(ctx, req.(*UserReq)) - } - return interceptor(ctx, in, info, handler) -} - -var _User_serviceDesc = grpc.ServiceDesc{ - ServiceName: "user.api.User", - HandlerType: (*UserServer)(nil), - Methods: []grpc.MethodDesc{ - { - MethodName: "Info", - Handler: _User_Info_Handler, - }, - }, - Streams: []grpc.StreamDesc{}, - Metadata: "api.proto", -} - -func (m *Info) Marshal() (dAtA []byte, err error) { - size := m.Size() - dAtA = make([]byte, size) - n, err := m.MarshalTo(dAtA) - if err != nil { - return nil, err - } - return dAtA[:n], nil -} - -func (m *Info) MarshalTo(dAtA []byte) (int, error) { - var i int - _ = i - var l int - _ = l - if m.Mid != 0 { - dAtA[i] = 0x8 - i++ - i = encodeVarintApi(dAtA, i, uint64(m.Mid)) - } - if len(m.Name) > 0 { - dAtA[i] = 0x12 - i++ - i = encodeVarintApi(dAtA, i, uint64(len(m.Name))) - i += copy(dAtA[i:], m.Name) - } - if len(m.Sex) > 0 { - dAtA[i] = 0x1a - i++ - i = encodeVarintApi(dAtA, i, uint64(len(m.Sex))) - i += copy(dAtA[i:], m.Sex) - } - if len(m.Face) > 0 { - dAtA[i] = 0x22 - i++ - i = encodeVarintApi(dAtA, i, uint64(len(m.Face))) - i += copy(dAtA[i:], m.Face) - } - if len(m.Sign) > 0 { - dAtA[i] = 0x2a - i++ - i = encodeVarintApi(dAtA, i, uint64(len(m.Sign))) - i += copy(dAtA[i:], m.Sign) - } - if m.XXX_unrecognized != nil { - i += copy(dAtA[i:], m.XXX_unrecognized) - } - return i, nil -} - -func (m *UserReq) Marshal() (dAtA []byte, err error) { - size := m.Size() - dAtA = make([]byte, size) - n, err := m.MarshalTo(dAtA) - if err != nil { - return nil, err - } - return dAtA[:n], nil -} - -func (m *UserReq) MarshalTo(dAtA []byte) (int, error) { - var i int - _ = i - var l int - _ = l - if m.Mid != 0 { - dAtA[i] = 0x8 - i++ - i = encodeVarintApi(dAtA, i, uint64(m.Mid)) - } - if m.XXX_unrecognized != nil { - i += copy(dAtA[i:], m.XXX_unrecognized) - } - return i, nil -} - -func (m *InfoReply) Marshal() (dAtA []byte, err error) { - size := m.Size() - dAtA = make([]byte, size) - n, err := m.MarshalTo(dAtA) - if err != nil { - return nil, err - } - return dAtA[:n], nil -} - -func (m *InfoReply) MarshalTo(dAtA []byte) (int, error) { - var i int - _ = i - var l int - _ = l - if m.Info != nil { - dAtA[i] = 0xa - i++ - i = encodeVarintApi(dAtA, i, uint64(m.Info.Size())) - n1, err := m.Info.MarshalTo(dAtA[i:]) - if err != nil { - return 0, err - } - i += n1 - } - if m.XXX_unrecognized != nil { - i += copy(dAtA[i:], m.XXX_unrecognized) - } - return i, nil -} - -func encodeVarintApi(dAtA []byte, offset int, v uint64) int { - for v >= 1<<7 { - dAtA[offset] = uint8(v&0x7f | 0x80) - v >>= 7 - offset++ - } - dAtA[offset] = uint8(v) - return offset + 1 -} -func (m *Info) Size() (n int) { - if m == nil { - return 0 - } - var l int - _ = l - if m.Mid != 0 { - n += 1 + sovApi(uint64(m.Mid)) - } - l = len(m.Name) - if l > 0 { - n += 1 + l + sovApi(uint64(l)) - } - l = len(m.Sex) - if l > 0 { - n += 1 + l + sovApi(uint64(l)) - } - l = len(m.Face) - if l > 0 { - n += 1 + l + sovApi(uint64(l)) - } - l = len(m.Sign) - if l > 0 { - n += 1 + l + sovApi(uint64(l)) - } - if m.XXX_unrecognized != nil { - n += len(m.XXX_unrecognized) - } - return n -} - -func (m *UserReq) Size() (n int) { - if m == nil { - return 0 - } - var l int - _ = l - if m.Mid != 0 { - n += 1 + sovApi(uint64(m.Mid)) - } - if m.XXX_unrecognized != nil { - n += len(m.XXX_unrecognized) - } - return n -} - -func (m *InfoReply) Size() (n int) { - if m == nil { - return 0 - } - var l int - _ = l - if m.Info != nil { - l = m.Info.Size() - n += 1 + l + sovApi(uint64(l)) - } - if m.XXX_unrecognized != nil { - n += len(m.XXX_unrecognized) - } - return n -} - -func sovApi(x uint64) (n int) { - for { - n++ - x >>= 7 - if x == 0 { - break - } - } - return n -} -func sozApi(x uint64) (n int) { - return sovApi(uint64((x << 1) ^ uint64((int64(x) >> 63)))) -} -func (m *Info) Unmarshal(dAtA []byte) error { - l := len(dAtA) - iNdEx := 0 - for iNdEx < l { - preIndex := iNdEx - var wire uint64 - for shift := uint(0); ; shift += 7 { - if shift >= 64 { - return ErrIntOverflowApi - } - if iNdEx >= l { - return io.ErrUnexpectedEOF - } - b := dAtA[iNdEx] - iNdEx++ - wire |= uint64(b&0x7F) << shift - if b < 0x80 { - break - } - } - fieldNum := int32(wire >> 3) - wireType := int(wire & 0x7) - if wireType == 4 { - return fmt.Errorf("proto: Info: wiretype end group for non-group") - } - if fieldNum <= 0 { - return fmt.Errorf("proto: Info: illegal tag %d (wire type %d)", fieldNum, wire) - } - switch fieldNum { - case 1: - if wireType != 0 { - return fmt.Errorf("proto: wrong wireType = %d for field Mid", wireType) - } - m.Mid = 0 - for shift := uint(0); ; shift += 7 { - if shift >= 64 { - return ErrIntOverflowApi - } - if iNdEx >= l { - return io.ErrUnexpectedEOF - } - b := dAtA[iNdEx] - iNdEx++ - m.Mid |= int64(b&0x7F) << shift - if b < 0x80 { - break - } - } - case 2: - if wireType != 2 { - return fmt.Errorf("proto: wrong wireType = %d for field Name", wireType) - } - var stringLen uint64 - for shift := uint(0); ; shift += 7 { - if shift >= 64 { - return ErrIntOverflowApi - } - if iNdEx >= l { - return io.ErrUnexpectedEOF - } - b := dAtA[iNdEx] - iNdEx++ - stringLen |= uint64(b&0x7F) << shift - if b < 0x80 { - break - } - } - intStringLen := int(stringLen) - if intStringLen < 0 { - return ErrInvalidLengthApi - } - postIndex := iNdEx + intStringLen - if postIndex < 0 { - return ErrInvalidLengthApi - } - if postIndex > l { - return io.ErrUnexpectedEOF - } - m.Name = string(dAtA[iNdEx:postIndex]) - iNdEx = postIndex - case 3: - if wireType != 2 { - return fmt.Errorf("proto: wrong wireType = %d for field Sex", wireType) - } - var stringLen uint64 - for shift := uint(0); ; shift += 7 { - if shift >= 64 { - return ErrIntOverflowApi - } - if iNdEx >= l { - return io.ErrUnexpectedEOF - } - b := dAtA[iNdEx] - iNdEx++ - stringLen |= uint64(b&0x7F) << shift - if b < 0x80 { - break - } - } - intStringLen := int(stringLen) - if intStringLen < 0 { - return ErrInvalidLengthApi - } - postIndex := iNdEx + intStringLen - if postIndex < 0 { - return ErrInvalidLengthApi - } - if postIndex > l { - return io.ErrUnexpectedEOF - } - m.Sex = string(dAtA[iNdEx:postIndex]) - iNdEx = postIndex - case 4: - if wireType != 2 { - return fmt.Errorf("proto: wrong wireType = %d for field Face", wireType) - } - var stringLen uint64 - for shift := uint(0); ; shift += 7 { - if shift >= 64 { - return ErrIntOverflowApi - } - if iNdEx >= l { - return io.ErrUnexpectedEOF - } - b := dAtA[iNdEx] - iNdEx++ - stringLen |= uint64(b&0x7F) << shift - if b < 0x80 { - break - } - } - intStringLen := int(stringLen) - if intStringLen < 0 { - return ErrInvalidLengthApi - } - postIndex := iNdEx + intStringLen - if postIndex < 0 { - return ErrInvalidLengthApi - } - if postIndex > l { - return io.ErrUnexpectedEOF - } - m.Face = string(dAtA[iNdEx:postIndex]) - iNdEx = postIndex - case 5: - if wireType != 2 { - return fmt.Errorf("proto: wrong wireType = %d for field Sign", wireType) - } - var stringLen uint64 - for shift := uint(0); ; shift += 7 { - if shift >= 64 { - return ErrIntOverflowApi - } - if iNdEx >= l { - return io.ErrUnexpectedEOF - } - b := dAtA[iNdEx] - iNdEx++ - stringLen |= uint64(b&0x7F) << shift - if b < 0x80 { - break - } - } - intStringLen := int(stringLen) - if intStringLen < 0 { - return ErrInvalidLengthApi - } - postIndex := iNdEx + intStringLen - if postIndex < 0 { - return ErrInvalidLengthApi - } - if postIndex > l { - return io.ErrUnexpectedEOF - } - m.Sign = string(dAtA[iNdEx:postIndex]) - iNdEx = postIndex - default: - iNdEx = preIndex - skippy, err := skipApi(dAtA[iNdEx:]) - if err != nil { - return err - } - if skippy < 0 { - return ErrInvalidLengthApi - } - if (iNdEx + skippy) < 0 { - return ErrInvalidLengthApi - } - if (iNdEx + skippy) > l { - return io.ErrUnexpectedEOF - } - m.XXX_unrecognized = append(m.XXX_unrecognized, dAtA[iNdEx:iNdEx+skippy]...) - iNdEx += skippy - } - } - - if iNdEx > l { - return io.ErrUnexpectedEOF - } - return nil -} -func (m *UserReq) Unmarshal(dAtA []byte) error { - l := len(dAtA) - iNdEx := 0 - for iNdEx < l { - preIndex := iNdEx - var wire uint64 - for shift := uint(0); ; shift += 7 { - if shift >= 64 { - return ErrIntOverflowApi - } - if iNdEx >= l { - return io.ErrUnexpectedEOF - } - b := dAtA[iNdEx] - iNdEx++ - wire |= uint64(b&0x7F) << shift - if b < 0x80 { - break - } - } - fieldNum := int32(wire >> 3) - wireType := int(wire & 0x7) - if wireType == 4 { - return fmt.Errorf("proto: UserReq: wiretype end group for non-group") - } - if fieldNum <= 0 { - return fmt.Errorf("proto: UserReq: illegal tag %d (wire type %d)", fieldNum, wire) - } - switch fieldNum { - case 1: - if wireType != 0 { - return fmt.Errorf("proto: wrong wireType = %d for field Mid", wireType) - } - m.Mid = 0 - for shift := uint(0); ; shift += 7 { - if shift >= 64 { - return ErrIntOverflowApi - } - if iNdEx >= l { - return io.ErrUnexpectedEOF - } - b := dAtA[iNdEx] - iNdEx++ - m.Mid |= int64(b&0x7F) << shift - if b < 0x80 { - break - } - } - default: - iNdEx = preIndex - skippy, err := skipApi(dAtA[iNdEx:]) - if err != nil { - return err - } - if skippy < 0 { - return ErrInvalidLengthApi - } - if (iNdEx + skippy) < 0 { - return ErrInvalidLengthApi - } - if (iNdEx + skippy) > l { - return io.ErrUnexpectedEOF - } - m.XXX_unrecognized = append(m.XXX_unrecognized, dAtA[iNdEx:iNdEx+skippy]...) - iNdEx += skippy - } - } - - if iNdEx > l { - return io.ErrUnexpectedEOF - } - return nil -} -func (m *InfoReply) Unmarshal(dAtA []byte) error { - l := len(dAtA) - iNdEx := 0 - for iNdEx < l { - preIndex := iNdEx - var wire uint64 - for shift := uint(0); ; shift += 7 { - if shift >= 64 { - return ErrIntOverflowApi - } - if iNdEx >= l { - return io.ErrUnexpectedEOF - } - b := dAtA[iNdEx] - iNdEx++ - wire |= uint64(b&0x7F) << shift - if b < 0x80 { - break - } - } - fieldNum := int32(wire >> 3) - wireType := int(wire & 0x7) - if wireType == 4 { - return fmt.Errorf("proto: InfoReply: wiretype end group for non-group") - } - if fieldNum <= 0 { - return fmt.Errorf("proto: InfoReply: illegal tag %d (wire type %d)", fieldNum, wire) - } - switch fieldNum { - case 1: - if wireType != 2 { - return fmt.Errorf("proto: wrong wireType = %d for field Info", wireType) - } - var msglen int - for shift := uint(0); ; shift += 7 { - if shift >= 64 { - return ErrIntOverflowApi - } - if iNdEx >= l { - return io.ErrUnexpectedEOF - } - b := dAtA[iNdEx] - iNdEx++ - msglen |= int(b&0x7F) << shift - if b < 0x80 { - break - } - } - if msglen < 0 { - return ErrInvalidLengthApi - } - postIndex := iNdEx + msglen - if postIndex < 0 { - return ErrInvalidLengthApi - } - if postIndex > l { - return io.ErrUnexpectedEOF - } - if m.Info == nil { - m.Info = &Info{} - } - if err := m.Info.Unmarshal(dAtA[iNdEx:postIndex]); err != nil { - return err - } - iNdEx = postIndex - default: - iNdEx = preIndex - skippy, err := skipApi(dAtA[iNdEx:]) - if err != nil { - return err - } - if skippy < 0 { - return ErrInvalidLengthApi - } - if (iNdEx + skippy) < 0 { - return ErrInvalidLengthApi - } - if (iNdEx + skippy) > l { - return io.ErrUnexpectedEOF - } - m.XXX_unrecognized = append(m.XXX_unrecognized, dAtA[iNdEx:iNdEx+skippy]...) - iNdEx += skippy - } - } - - if iNdEx > l { - return io.ErrUnexpectedEOF - } - return nil -} -func skipApi(dAtA []byte) (n int, err error) { - l := len(dAtA) - iNdEx := 0 - for iNdEx < l { - var wire uint64 - for shift := uint(0); ; shift += 7 { - if shift >= 64 { - return 0, ErrIntOverflowApi - } - if iNdEx >= l { - return 0, io.ErrUnexpectedEOF - } - b := dAtA[iNdEx] - iNdEx++ - wire |= (uint64(b) & 0x7F) << shift - if b < 0x80 { - break - } - } - wireType := int(wire & 0x7) - switch wireType { - case 0: - for shift := uint(0); ; shift += 7 { - if shift >= 64 { - return 0, ErrIntOverflowApi - } - if iNdEx >= l { - return 0, io.ErrUnexpectedEOF - } - iNdEx++ - if dAtA[iNdEx-1] < 0x80 { - break - } - } - return iNdEx, nil - case 1: - iNdEx += 8 - return iNdEx, nil - case 2: - var length int - for shift := uint(0); ; shift += 7 { - if shift >= 64 { - return 0, ErrIntOverflowApi - } - if iNdEx >= l { - return 0, io.ErrUnexpectedEOF - } - b := dAtA[iNdEx] - iNdEx++ - length |= (int(b) & 0x7F) << shift - if b < 0x80 { - break - } - } - if length < 0 { - return 0, ErrInvalidLengthApi - } - iNdEx += length - if iNdEx < 0 { - return 0, ErrInvalidLengthApi - } - return iNdEx, nil - case 3: - for { - var innerWire uint64 - var start int = iNdEx - for shift := uint(0); ; shift += 7 { - if shift >= 64 { - return 0, ErrIntOverflowApi - } - if iNdEx >= l { - return 0, io.ErrUnexpectedEOF - } - b := dAtA[iNdEx] - iNdEx++ - innerWire |= (uint64(b) & 0x7F) << shift - if b < 0x80 { - break - } - } - innerWireType := int(innerWire & 0x7) - if innerWireType == 4 { - break - } - next, err := skipApi(dAtA[start:]) - if err != nil { - return 0, err - } - iNdEx = start + next - if iNdEx < 0 { - return 0, ErrInvalidLengthApi - } - } - return iNdEx, nil - case 4: - return iNdEx, nil - case 5: - iNdEx += 4 - return iNdEx, nil - default: - return 0, fmt.Errorf("proto: illegal wireType %d", wireType) - } - } - panic("unreachable") -} - -var ( - ErrInvalidLengthApi = fmt.Errorf("proto: negative length found during unmarshaling") - ErrIntOverflowApi = fmt.Errorf("proto: integer overflow") -) diff --git a/example/protobuf/api.proto b/example/protobuf/api.proto deleted file mode 100644 index 23f65d47d..000000000 --- a/example/protobuf/api.proto +++ /dev/null @@ -1,35 +0,0 @@ -syntax = "proto3"; - -package user.api; - -import "github.com/gogo/protobuf/gogoproto/gogo.proto"; -import "google/protobuf/empty.proto"; - -option go_package = "api"; - -enum UserErrCode { - OK = 0; - UserNotExist = -404; - UserUpdateNameFailed = 10000; -} - -message Info { - int64 mid = 1 [(gogoproto.jsontag) = "mid"]; - string name = 2 [(gogoproto.jsontag) = "name"]; - string sex = 3 [(gogoproto.jsontag) = "sex"]; - string face = 4 [(gogoproto.jsontag) = "face"]; - string sign = 5 [(gogoproto.jsontag) = "sign"]; -} - -message UserReq { - int64 mid = 1 [(gogoproto.moretags) = "validate:\"gt=0,required\""]; -} - -message InfoReply { - Info info = 1; -} - -service User { - rpc Info(UserReq) returns (InfoReply); - rpc Card(UserReq) returns (google.protobuf.Empty); -} diff --git a/example/protobuf/api.swagger.json b/example/protobuf/api.swagger.json deleted file mode 100644 index 32266a239..000000000 --- a/example/protobuf/api.swagger.json +++ /dev/null @@ -1,96 +0,0 @@ -{ - "swagger": "2.0", - "info": { - "title": "api.proto", - "version": "" - }, - "schemes": [ - "http", - "https" - ], - "consumes": [ - "application/json", - "multipart/form-data" - ], - "produces": [ - "application/json" - ], - "paths": { - "/user.api.User/Info": { - "get": { - "summary": "/user.api.User/Info", - "responses": { - "200": { - "description": "A successful response.", - "schema": { - "type": "object", - "properties": { - "code": { - "type": "integer" - }, - "message": { - "type": "string" - }, - "data": { - "$ref": "#/definitions/.user.api.InfoReply" - } - } - } - } - }, - "parameters": [ - { - "name": "mid", - "in": "query", - "required": true, - "type": "integer" - } - ], - "tags": [ - "user.api.User" - ] - } - } - }, - "definitions": { - ".user.api.Info": { - "type": "object", - "properties": { - "mid": { - "type": "integer" - }, - "name": { - "type": "string" - }, - "sex": { - "type": "string" - }, - "face": { - "type": "string" - }, - "sign": { - "type": "string" - } - } - }, - ".user.api.InfoReply": { - "type": "object", - "properties": { - "info": { - "$ref": "#/definitions/.user.api.Info" - } - } - }, - ".user.api.UserReq": { - "type": "object", - "properties": { - "mid": { - "type": "integer" - } - }, - "required": [ - "mid" - ] - } - } -} \ No newline at end of file diff --git a/example/protobuf/gen.sh b/example/protobuf/gen.sh deleted file mode 100644 index 96984776c..000000000 --- a/example/protobuf/gen.sh +++ /dev/null @@ -1,3 +0,0 @@ -#!/bin/bash - -kratos tool protoc api.proto diff --git a/go.mod b/go.mod index ce4196b74..9880ce6c1 100644 --- a/go.mod +++ b/go.mod @@ -1,70 +1,16 @@ -module github.com/go-kratos/kratos +module github.com/go-kratos/kratos/v2 -go 1.13 +go 1.15 require ( - github.com/BurntSushi/toml v0.3.1 - github.com/HdrHistogram/hdrhistogram-go v1.0.1 // indirect - github.com/StackExchange/wmi v0.0.0-20190523213315-cbe66965904d // indirect - github.com/aristanetworks/goarista v0.0.0-20190912214011-b54698eaaca6 // indirect - github.com/coreos/go-semver v0.3.0 // indirect - github.com/cpuguy83/go-md2man/v2 v2.0.0 // indirect - github.com/cznic/b v0.0.0-20181122101859-a26611c4d92d // indirect - github.com/cznic/mathutil v0.0.0-20181122101859-297441e03548 // indirect - github.com/cznic/strutil v0.0.0-20181122101858-275e90344537 // indirect - github.com/dgryski/go-farm v0.0.0-20190423205320-6a90982ecee2 - github.com/dustin/go-humanize v1.0.0 // indirect - github.com/fatih/color v1.10.0 - github.com/fsnotify/fsnotify v1.4.7 - github.com/go-ole/go-ole v1.2.4 // indirect - github.com/go-playground/locales v0.12.1 // indirect - github.com/go-playground/universal-translator v0.16.0 // indirect - github.com/go-sql-driver/mysql v1.4.1 - github.com/go-zookeeper/zk v1.0.1 - github.com/gobuffalo/packr/v2 v2.7.1 - github.com/gogo/protobuf v1.3.1 - github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e // indirect - github.com/golang/mock v1.3.1 // indirect - github.com/golang/protobuf v1.3.5 - github.com/google/uuid v1.1.1 // indirect - github.com/gorilla/websocket v1.4.2 // indirect - github.com/grpc-ecosystem/go-grpc-middleware v1.2.0 // indirect - github.com/grpc-ecosystem/grpc-gateway v1.14.3 // indirect - github.com/leodido/go-urn v1.1.0 // indirect - github.com/montanaflynn/stats v0.5.0 - github.com/opentracing/opentracing-go v1.1.0 - github.com/openzipkin/zipkin-go v0.2.1 - github.com/otokaze/mock v0.0.0-20190125081256-8282b7a7c7c3 - github.com/philchia/agollo/v4 v4.1.1 - github.com/pkg/errors v0.8.1 - github.com/prometheus/client_golang v1.5.1 - github.com/prometheus/procfs v0.0.11 // indirect - github.com/remyoudompheng/bigfft v0.0.0-20190728182440-6a916e37a237 // indirect - github.com/rogpeppe/go-internal v1.5.0 // indirect - github.com/russross/blackfriday/v2 v2.1.0 // indirect - github.com/samuel/go-zookeeper v0.0.0-20180130194729-c4fab1ac1bec // indirect - github.com/shirou/gopsutil v2.19.11+incompatible - github.com/siddontang/go v0.0.0-20180604090527-bdc77568d726 - github.com/sirupsen/logrus v1.5.0 - github.com/spf13/pflag v1.0.5 // indirect - github.com/stretchr/testify v1.6.1 - github.com/tmc/grpc-websocket-proxy v0.0.0-20200122045848-3419fae592fc // indirect - github.com/tsuna/gohbase v0.0.0-20190502052937-24ffed0537aa - github.com/uber/jaeger-client-go v2.25.0+incompatible - github.com/uber/jaeger-lib v2.4.0+incompatible // indirect - github.com/urfave/cli/v2 v2.3.0 - go.etcd.io/etcd v0.0.0-20200402134248-51bdeb39e698 - go.uber.org/atomic v1.6.0 - golang.org/x/crypto v0.0.0-20200323165209-0ec3e9974c59 // indirect - golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e - golang.org/x/sys v0.0.0-20201202213521-69691e467435 // indirect - golang.org/x/time v0.0.0-20191024005414-555d28b269f0 // indirect - golang.org/x/tools v0.0.0-20191105231337-689d0f08e67a - google.golang.org/appengine v1.6.1 // indirect - google.golang.org/genproto v0.0.0-20200402124713-8ff61da6d932 - google.golang.org/grpc v1.29.1 - gopkg.in/go-playground/assert.v1 v1.2.1 // indirect - gopkg.in/go-playground/validator.v9 v9.29.1 - gopkg.in/yaml.v2 v2.4.0 - sigs.k8s.io/yaml v1.2.0 // indirect + github.com/fsnotify/fsnotify v1.4.9 + github.com/golang/protobuf v1.4.3 + github.com/google/uuid v1.1.2 + github.com/gorilla/mux v1.8.0 + github.com/imdario/mergo v0.3.6 + github.com/opentracing/opentracing-go v1.2.0 + golang.org/x/sync v0.0.0-20190423024810-112230192c58 + google.golang.org/genproto v0.0.0-20210114201628-6edceaf6022f + google.golang.org/grpc v1.35.0 + google.golang.org/protobuf v1.25.0 ) diff --git a/go.sum b/go.sum index c720fc096..f4c905468 100644 --- a/go.sum +++ b/go.sum @@ -1,535 +1,108 @@ cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= -github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= -github.com/DataDog/zstd v1.3.6-0.20190409195224-796139022798/go.mod h1:1jcaCB/ufaK+sKp1NBhlGmpz41jOoPQ35bpF36t7BBo= -github.com/HdrHistogram/hdrhistogram-go v1.0.1 h1:GX8GAYDuhlFQnI2fRDHQhTlkHMz8bEn0jTI6LJU0mpw= -github.com/HdrHistogram/hdrhistogram-go v1.0.1/go.mod h1:BWJ+nMSHY3L41Zj7CA3uXnloDp7xxV0YvstAE7nKTaM= -github.com/Shopify/sarama v1.19.0/go.mod h1:FVkBWblsNy7DGZRfXLU0O9RCGt5g3g3yEuWXgklEdEo= -github.com/Shopify/sarama v1.23.1/go.mod h1:XLH1GYJnLVE0XCr6KdJGVJRTwY30moWNJ4sERjXX6fs= -github.com/Shopify/toxiproxy v2.1.4+incompatible/go.mod h1:OXgGpZ6Cli1/URJOF1DMxUHB2q5Ap20/P/eIdh4G0pI= -github.com/StackExchange/wmi v0.0.0-20190523213315-cbe66965904d h1:G0m3OIz70MZUWq3EgK3CesDbo8upS2Vm9/P3FtgI+Jk= -github.com/StackExchange/wmi v0.0.0-20190523213315-cbe66965904d/go.mod h1:3eOhrUMpNV+6aFIbp5/iudMxNCF27Vw2OZgy4xEx0Fg= -github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= -github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= -github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= -github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= -github.com/antihax/optional v0.0.0-20180407024304-ca021399b1a6/go.mod h1:V8iCPQYkqmusNa815XgQio277wI47sdRh1dUOLdyC6Q= -github.com/aristanetworks/fsnotify v1.4.2/go.mod h1:D/rtu7LpjYM8tRJphJ0hUBYpjai8SfX+aSNsWDTq/Ks= -github.com/aristanetworks/glog v0.0.0-20180419172825-c15b03b3054f/go.mod h1:KASm+qXFKs/xjSoWn30NrWBBvdTTQq+UjkhjEJHfSFA= -github.com/aristanetworks/goarista v0.0.0-20190912214011-b54698eaaca6 h1:6bZNnQcA2fkzH9AhZXbp2nDqbWa4bBqFeUb70Zq1HBM= -github.com/aristanetworks/goarista v0.0.0-20190912214011-b54698eaaca6/go.mod h1:Z4RTxGAuYhPzcq8+EdRM+R8M48Ssle2TsWtwRKa+vns= -github.com/aristanetworks/splunk-hec-go v0.3.3/go.mod h1:1VHO9r17b0K7WmOlLb9nTk/2YanvOEnLMUgsFrxBROc= -github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8= -github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= -github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= -github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= -github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= -github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs= github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= -github.com/cespare/xxhash/v2 v2.1.1 h1:6MnRN8NT7+YBpUIWxHtefFZOKTAPgGjpQSxqLNn0+qY= -github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= -github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= -github.com/cockroachdb/datadriven v0.0.0-20190809214429-80d97fb3cbaa h1:OaNxuTZr7kxeODyLWsRMC+OD03aFUH+mW6r2d+MWa5Y= -github.com/cockroachdb/datadriven v0.0.0-20190809214429-80d97fb3cbaa/go.mod h1:zn76sxSg3SzpJ0PPJaLDCu+Bu0Lg3sKTORVIj19EIF8= -github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE= -github.com/coreos/go-etcd v2.0.0+incompatible/go.mod h1:Jez6KQU2B/sWsbdaef3ED8NzMklzPG4d5KIOhIy30Tk= -github.com/coreos/go-semver v0.2.0 h1:3Jm3tLmsgAYcjC+4Up7hJrFBPr+n7rAqYeSw/SZazuY= -github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= -github.com/coreos/go-semver v0.3.0 h1:wkHLiw0WNATZnSG7epLsujiMCgPAc9xhjJ4tgnAxmfM= -github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= -github.com/coreos/go-systemd/v22 v22.0.0 h1:XJIw/+VlJ+87J+doOxznsAWIdmWuViOVhkQamW5YV28= -github.com/coreos/go-systemd/v22 v22.0.0/go.mod h1:xO0FLkIi5MaZafQlIrOotqXZ90ih+1atmu1JpKERPPk= -github.com/cpuguy83/go-md2man v1.0.10 h1:BSKMNlYxDvnunlTymqtgONjNnaRV1sTpcovwwjF22jk= -github.com/cpuguy83/go-md2man v1.0.10/go.mod h1:SmD6nW6nTyfqj6ABTjUi3V3JVMnlJmwcJI5acqYI6dE= -github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d h1:U+s90UTSYgptZMwQh2aRr3LuazLJIa+Pg3Kc1ylSYVY= -github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= -github.com/cpuguy83/go-md2man/v2 v2.0.0 h1:EoUDS0afbrsXAZ9YQ9jdu/mZ2sXgT1/2yyNng4PGlyM= -github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= -github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY= -github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= -github.com/cznic/b v0.0.0-20181122101859-a26611c4d92d h1:SwD98825d6bdB+pEuTxWOXiSjBrHdOl/UVp75eI7JT8= -github.com/cznic/b v0.0.0-20181122101859-a26611c4d92d/go.mod h1:URriBxXwVq5ijiJ12C7iIZqlA69nTlI+LgI6/pwftG8= -github.com/cznic/mathutil v0.0.0-20181122101859-297441e03548 h1:iwZdTE0PVqJCos1vaoKsclOGD3ADKpshg3SRtYBbwso= -github.com/cznic/mathutil v0.0.0-20181122101859-297441e03548/go.mod h1:e6NPNENfs9mPDVNRekM7lKScauxd5kXTr1Mfyig6TDM= -github.com/cznic/strutil v0.0.0-20181122101858-275e90344537 h1:MZRmHqDBd0vxNwenEbKSQqRVT24d3C05ft8kduSwlqM= -github.com/cznic/strutil v0.0.0-20181122101858-275e90344537/go.mod h1:AHHPPPXTw0h6pVabbcbyGRK1DckRn7r/STdZEeIDzZc= +github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= +github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= -github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/dgrijalva/jwt-go v3.2.0+incompatible h1:7qlOGliEKZXTDg6OTjfoBKDXWrumCAMpl/TFQ4/5kLM= -github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= -github.com/dgryski/go-farm v0.0.0-20190423205320-6a90982ecee2 h1:tdlZCpZ/P9DhczCTSixgIKmwPv6+wP5DGjqLYw5SUiA= -github.com/dgryski/go-farm v0.0.0-20190423205320-6a90982ecee2/go.mod h1:SqUrOPUnsFjfmXRMNPybcSiG0BgUW2AuFH8PAnS2iTw= -github.com/dustin/go-humanize v0.0.0-20171111073723-bb3d318650d4 h1:qk/FSDDxo05wdJH28W+p5yivv7LuLYLRXPPD8KQCtZs= -github.com/dustin/go-humanize v0.0.0-20171111073723-bb3d318650d4/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= -github.com/dustin/go-humanize v1.0.0 h1:VSnTsYCnlFHaM2/igO1h6X3HA71jcobQuxemgkq4zYo= -github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= -github.com/eapache/go-resiliency v1.1.0/go.mod h1:kFI+JgMyC7bLPUVY133qvEBtVayf5mFgVsvEsIPBvNs= -github.com/eapache/go-xerial-snappy v0.0.0-20180814174437-776d5712da21/go.mod h1:+020luEh2TKB4/GOp8oxxtq0Daoen/Cii55CzbTV6DU= -github.com/eapache/queue v1.1.0/go.mod h1:6eCeP0CKFpHLu8blIFXhExK/dRa7WDZfr6jVFPTqq+I= -github.com/envoyproxy/go-control-plane v0.6.9/go.mod h1:SBwIajubJHhxtWwsL9s8ss4safvEdbitLhGGK48rN6g= github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= -github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= +github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= -github.com/fatih/color v1.7.0 h1:DkWD4oS2D8LGGgTQ6IvwJJXSL5Vp2ffcQg58nFV38Ys= -github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= -github.com/fatih/color v1.10.0 h1:s36xzo75JdqLaaWoiEHk767eHiwo0598uUxyfiPkDsg= -github.com/fatih/color v1.10.0/go.mod h1:ELkj/draVOlAH/xkhN6mQ50Qd0MPOk5AAr3maGEBuJM= -github.com/fsnotify/fsnotify v1.4.7 h1:IXs+QLmnXW2CcXuY+8Mzv/fWEsPGWxqefPtCP5CnV9I= -github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= -github.com/garyburd/redigo v1.6.0/go.mod h1:NR3MbYisc3/PwhQ00EMzDiPmrwpPxAn5GI05/YaO1SY= -github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= -github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= -github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= -github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= -github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= -github.com/go-ole/go-ole v1.2.4 h1:nNBDSCOigTSiarFpYE9J/KtEA1IOW4CNeqT9TQDqCxI= -github.com/go-ole/go-ole v1.2.4/go.mod h1:XCwSNxSkXRo4vlyPy93sltvi/qJq0jqQhjqQNIwKuxM= -github.com/go-playground/locales v0.12.1 h1:2FITxuFt/xuCNP1Acdhv62OzaCiviiE4kotfhkmOqEc= -github.com/go-playground/locales v0.12.1/go.mod h1:IUMDtCfWo/w/mtMfIE/IG2K+Ey3ygWanZIBtBW0W2TM= -github.com/go-playground/universal-translator v0.16.0 h1:X++omBR/4cE2MNg91AoC3rmGrCjJ8eAeUP/K/EKx4DM= -github.com/go-playground/universal-translator v0.16.0/go.mod h1:1AnU7NaIRDWWzGEKwgtJRd2xk99HeFyHw3yid4rvQIY= -github.com/go-sql-driver/mysql v1.4.1 h1:g24URVg0OFbNUTx9qqY1IRZ9D9z3iPyi5zKhQZpNwpA= -github.com/go-sql-driver/mysql v1.4.1/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w= -github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= -github.com/go-zookeeper/zk v1.0.1 h1:LmXNmSnkNsNKai+aDu6sHRr8ZJzIrHJo8z8Z4sm8cT8= -github.com/go-zookeeper/zk v1.0.1/go.mod h1:gpJdHazfkmlg4V0rt0vYeHYJHSL8hHFwV0qOd+HRTJE= -github.com/gobuffalo/envy v1.7.0/go.mod h1:n7DRkBerg/aorDM8kbduw5dN3oXGswK5liaSCx4T5NI= -github.com/gobuffalo/envy v1.7.1 h1:OQl5ys5MBea7OGCdvPbBJWRgnhC/fGona6QKfvFeau8= -github.com/gobuffalo/envy v1.7.1/go.mod h1:FurDp9+EDPE4aIUS3ZLyD+7/9fpx7YRt/ukY6jIHf0w= -github.com/gobuffalo/logger v1.0.1 h1:ZEgyRGgAm4ZAhAO45YXMs5Fp+bzGLESFewzAVBMKuTg= -github.com/gobuffalo/logger v1.0.1/go.mod h1:2zbswyIUa45I+c+FLXuWl9zSWEiVuthsk8ze5s8JvPs= -github.com/gobuffalo/packd v0.3.0 h1:eMwymTkA1uXsqxS0Tpoop3Lc0u3kTfiMBE6nKtQU4g4= -github.com/gobuffalo/packd v0.3.0/go.mod h1:zC7QkmNkYVGKPw4tHpBQ+ml7W/3tIebgeo1b36chA3Q= -github.com/gobuffalo/packr/v2 v2.7.1 h1:n3CIW5T17T8v4GGK5sWXLVWJhCz7b5aNLSxW6gYim4o= -github.com/gobuffalo/packr/v2 v2.7.1/go.mod h1:qYEvAazPaVxy7Y7KR0W8qYEE+RymX74kETFqjFoFlOc= -github.com/godbus/dbus/v5 v5.0.3/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= -github.com/gogo/googleapis v1.1.0/go.mod h1:gf4bu3Q80BeJ6H1S1vYPm8/ELATdvryBaNFGgqEef3s= -github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= -github.com/gogo/protobuf v1.2.0/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= -github.com/gogo/protobuf v1.2.1 h1:/s5zKNz0uPFCZ5hddgPdo2TK2TVrUNMn0OOX8/aZMTE= -github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4= -github.com/gogo/protobuf v1.3.1 h1:DqDEcV5aeaTmdFBePNpYsp3FlcVH/2ISVVM9Qf8PSls= -github.com/gogo/protobuf v1.3.1/go.mod h1:SlYgWuQ5SjCEi6WLHjHCa1yvBfUnHcTbrrZtXPKa29o= -github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b h1:VKtxabqXZkF25pY9ekfRL6a582T4P37/31XEstQ5p58= +github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4= +github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= -github.com/golang/groupcache v0.0.0-20160516000752-02826c3e7903/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= -github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e h1:1r7pUrabqp18hOBcwBwiTsbnFeTZHV9eER/QT5JVZxY= -github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= -github.com/golang/mock v1.3.1 h1:qGJ6qTW+x6xX/my+8YUVl4WNpX9B7+/l2tRsHGZ7f2s= -github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y= github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= -github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= -github.com/golang/protobuf v1.3.2 h1:6nsPYzhq5kReh6QImI3k5qWzO4PEbvbIW2cwSfR/6xs= github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= -github.com/golang/protobuf v1.3.3 h1:gyjaxf+svBWX08ZjK86iN9geUJF0H6gp2IRKX6Nf6/I= -github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= -github.com/golang/protobuf v1.3.5 h1:F768QJ1E9tib+q5Sc8MkdJi1RxLTbRcTf8LJV56aRls= -github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk= -github.com/golang/snappy v0.0.0-20180518054509-2e65f85255db/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= -github.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= -github.com/google/btree v1.0.0 h1:0udJVsspx3VBr5FwtLhQQtuAsVc79tTq0ocGIPAU6qo= -github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= +github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= +github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= +github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= +github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= +github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= +github.com/golang/protobuf v1.4.1 h1:ZFgWrT+bLgsYPirOnRfKLYJLvssAegOj/hgyMFdJZe0= +github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= +github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= +github.com/golang/protobuf v1.4.3 h1:JjCZWpVbqXDqFVmTfYWEVTMIYrL/NPdPSCHPJ0T/raM= +github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= -github.com/google/go-cmp v0.4.0 h1:xsAVV57WRhGj6kEIi8ReJzQlHHqcBYCElAvkovg3B/4= github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.2 h1:X2ev0eStA3AbceY54o37/0PQ/UWqKEiiO2dKL5OPaFM= -github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= -github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= -github.com/google/uuid v1.0.0 h1:b4Gk+7WdP/d3HZH8EJsZpvV7EtDOgaZLtnaNGIu1adA= -github.com/google/uuid v1.0.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/google/uuid v1.1.1 h1:Gkbcsh/GbpXz7lPftLA3P6TYMwjCLYm83jiFQZF/3gY= -github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/gorilla/context v1.1.1/go.mod h1:kBGZzfjB9CEq2AlWe17Uuf7NDRt0dE0s8S51q0aT7Yg= -github.com/gorilla/mux v1.6.2/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs= -github.com/gorilla/websocket v0.0.0-20170926233335-4201258b820c/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ= -github.com/gorilla/websocket v1.4.2 h1:+/TMaTYc4QFitKJxsQ7Yye35DkWvkdLcvGKqM+x0Ufc= -github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= -github.com/grpc-ecosystem/go-grpc-middleware v1.0.1-0.20190118093823-f849b5445de4 h1:z53tR0945TRRQO/fLEVPI6SMv7ZflF0TEaTAoU7tOzg= -github.com/grpc-ecosystem/go-grpc-middleware v1.0.1-0.20190118093823-f849b5445de4/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs= -github.com/grpc-ecosystem/go-grpc-middleware v1.2.0 h1:0IKlLyQ3Hs9nDaiK5cSHAGmcQEIC8l2Ts1u6x5Dfrqg= -github.com/grpc-ecosystem/go-grpc-middleware v1.2.0/go.mod h1:mJzapYve32yjrKlk9GbyCZHuPgZsrbyIbyKhSzOpg6s= -github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0 h1:Ovs26xHkKqVztRpIrF/92BcuyuQ/YW4NSIpoGtfXNho= -github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk= -github.com/grpc-ecosystem/grpc-gateway v1.9.5 h1:UImYN5qQ8tuGpGE16ZmjvcTtTw24zw1QAp/SlnNrZhI= -github.com/grpc-ecosystem/grpc-gateway v1.9.5/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY= -github.com/grpc-ecosystem/grpc-gateway v1.14.3 h1:OCJlWkOUoTnl0neNGlf4fUm3TmbEtguw7vR+nGtnDjY= -github.com/grpc-ecosystem/grpc-gateway v1.14.3/go.mod h1:6CwZWGDSPRJidgKAtJVvND6soZe6fT7iteq8wDPdhb0= -github.com/hashicorp/go-uuid v1.0.1/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= -github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= -github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= -github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM= -github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= -github.com/influxdata/influxdb1-client v0.0.0-20190809212627-fc22c7df067e/go.mod h1:qj24IKcXYK6Iy9ceXlo3Tc+vtHo9lIhSX5JddghvEPo= -github.com/jcmturner/gofork v0.0.0-20190328161633-dc7c13fece03/go.mod h1:MK8+TM0La+2rjBD4jE12Kj1pCCxK7d2LK/UM3ncEo0o= -github.com/joho/godotenv v1.3.0 h1:Zjp+RcGpHhGlrMbJzXTrZZPrWj+1vfm90La1wgB6Bhc= -github.com/joho/godotenv v1.3.0/go.mod h1:7hK45KPybAkOC6peb+G5yklZfMxEjkZhHbwpqxOKXbg= -github.com/jonboulle/clockwork v0.1.0 h1:VKV+ZcuP6l3yW9doeqz6ziZGgcynBVQO+obU0+0hcPo= -github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo= -github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= -github.com/json-iterator/go v1.1.7 h1:KfgG9LzI+pYjr4xvmz/5H4FXjokeP+rlHLhv3iH62Fo= -github.com/json-iterator/go v1.1.7/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= -github.com/json-iterator/go v1.1.9 h1:9yzud/Ht36ygwatGx56VwCZtlI/2AD15T1X2sjSuGns= -github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= -github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= -github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q= -github.com/kisielk/errcheck v1.2.0/go.mod h1:/BMXB+zMLi60iA8Vv6Ksmxu/1UDYcXs4uQLJ+jE2L00= -github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= -github.com/klauspost/cpuid v1.2.1/go.mod h1:Pj4uuM528wm8OyEC2QMXAi2YiTZ96dNQPGgoMS4s3ek= -github.com/klauspost/reedsolomon v1.9.2/go.mod h1:CwCi+NUr9pqSVktrkN+Ondf06rkhYZ/pcNv7fu+8Un4= -github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= -github.com/konsorten/go-windows-terminal-sequences v1.0.2 h1:DB17ag19krx9CFsz4o3enTrPXyIXCl+2iCXH/aMAp9s= -github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= -github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= -github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= -github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= -github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= -github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= -github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= -github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= -github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= -github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= -github.com/leodido/go-urn v1.1.0 h1:Sm1gr51B1kKyfD2BlRcLSiEkffoG96g6TPv6eRoEiB8= -github.com/leodido/go-urn v1.1.0/go.mod h1:+cyI34gQWZcE1eQU7NVgKkkzdXDQHr1dBMtdAPozLkw= -github.com/lyft/protoc-gen-validate v0.0.13/go.mod h1:XbGvPuh87YZc5TdIa2/I4pLk0QoUACkjt2znoq26NVQ= -github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= -github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= -github.com/mattn/go-colorable v0.1.8 h1:c1ghPdyEDarC70ftn0y+A/Ee++9zz8ljHG1b13eJ0s8= -github.com/mattn/go-colorable v0.1.8/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= -github.com/mattn/go-isatty v0.0.4/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= -github.com/mattn/go-isatty v0.0.12 h1:wuysRhFDzyxgEmMf5xjvJ2M9dZoWAXNNr5LSBS7uHXY= -github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= -github.com/mattn/go-runewidth v0.0.2/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU= -github.com/matttproud/golang_protobuf_extensions v1.0.1 h1:4hp9jkHxhMHkqkrB3Ix0jegS5sx/RkqARlsWZ6pIwiU= -github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= -github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= -github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= -github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= -github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= -github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= -github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= -github.com/modern-go/reflect2 v1.0.1 h1:9f412s+6RmYXLWZSEzVVgPGK7C2PphHj5RJrvfx9AWI= -github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= -github.com/montanaflynn/stats v0.5.0 h1:2EkzeTSqBB4V4bJwWrt5gIIrZmpJBcoIRGS2kWLgzmk= -github.com/montanaflynn/stats v0.5.0/go.mod h1:wL8QJuTMNUDYhXwkmfOly8iTdp5TEcJFWZD2D7SIkUc= -github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= -github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs= -github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= -github.com/olekukonko/tablewriter v0.0.0-20170122224234-a0225b3f23b5/go.mod h1:vsDQFd/mU46D+Z4whnwzcISnGGzXWMclvtLoiIKAKIo= -github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= -github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= -github.com/onsi/ginkgo v1.10.1/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= -github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= -github.com/onsi/gomega v1.7.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= -github.com/openconfig/gnmi v0.0.0-20190823184014-89b2bf29312c/go.mod h1:t+O9It+LKzfOAhKTT5O0ehDix+MTqbtT0T9t+7zzOvc= -github.com/openconfig/reference v0.0.0-20190727015836-8dfd928c9696/go.mod h1:ym2A+zigScwkSEb/cVQB0/ZMpU3rqiH6X7WRRsxgOGw= -github.com/opentracing/opentracing-go v1.1.0 h1:pWlfV3Bxv7k65HYwkikxat0+s3pV4bsqf19k25Ur8rU= -github.com/opentracing/opentracing-go v1.1.0/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o= -github.com/openzipkin/zipkin-go v0.2.1 h1:noL5/5Uf1HpVl3wNsfkZhIKbSWCVi5jgqkONNx8PXcA= -github.com/openzipkin/zipkin-go v0.2.1/go.mod h1:NaW6tEwdmWMaCDZzg8sh+IBNOxHMPnhQw8ySjnjRyN4= -github.com/otokaze/mock v0.0.0-20190125081256-8282b7a7c7c3 h1:zjmNboC3QFuMdJSaZJ7Qvi3HUxWXPdj7wb3rc4jH5HI= -github.com/otokaze/mock v0.0.0-20190125081256-8282b7a7c7c3/go.mod h1:pLR8n2aimFxvvDJ6n8JuQWthMGezCYMjuhlaTjPTZf0= -github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic= -github.com/philchia/agollo/v4 v4.1.1 h1:6AFFJ8+J1Ru5SPclUyMWLUmd62Ak/1fqZrEthiL6UeE= -github.com/philchia/agollo/v4 v4.1.1/go.mod h1:SBdQmfqqu/XCWJ1MDzYcCL3X+p3VJ+uQBy0nxxqjexg= -github.com/pierrec/lz4 v0.0.0-20190327172049-315a67e90e41/go.mod h1:3/3N9NVKO0jef7pBehbT1qWhCMrIgbYNnFAZCqQ5LRc= -github.com/pierrec/lz4 v1.0.2-0.20190131084431-473cd7ce01a1/go.mod h1:3/3N9NVKO0jef7pBehbT1qWhCMrIgbYNnFAZCqQ5LRc= -github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= -github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I= -github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= -github.com/pkg/profile v1.2.1/go.mod h1:hJw3o1OdXxsrSjjVksARp5W95eeEaEfptyVZyv6JUPA= +github.com/google/go-cmp v0.5.0 h1:/QaMHBdZ26BB3SSst0Iwl10Epc+xhTquomWX0oZEB6w= +github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/uuid v1.1.2 h1:EVhdT+1Kseyi1/pUmXKaFxYsDNy9RQYkMWRH68J/W7Y= +github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI= +github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So= +github.com/imdario/mergo v0.3.6 h1:xTNEAn+kxVO7dTZGu0CegyqKZmoWFI0rF8UxjlB2d28= +github.com/imdario/mergo v0.3.6/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA= +github.com/opentracing/opentracing-go v1.2.0 h1:uEJPy/1a5RIPAJ0Ov+OIO8OxWu77jEv+1B0VhjKrZUs= +github.com/opentracing/opentracing-go v1.2.0/go.mod h1:GxEUsuufX4nBwe+T+Wl9TAgYrxe9dPLANfrWvHYVTgc= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= -github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo= -github.com/prometheus/client_golang v1.1.0 h1:BQ53HtBmfOitExawJ6LokA4x8ov/z0SYYb0+HxJfRI8= -github.com/prometheus/client_golang v1.1.0/go.mod h1:I1FGZT9+L76gKKOs5djB6ezCbFQP1xR9D75/vuwEF3g= -github.com/prometheus/client_golang v1.5.1 h1:bdHYieyGlH+6OLEk2YQha8THib30KP0/yD0YH9m6xcA= -github.com/prometheus/client_golang v1.5.1/go.mod h1:e9GMxYsXl05ICDXkRhurwBS4Q3OK1iX/F2sw+iXX5zU= -github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= -github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90 h1:S/YWwWx/RA8rT8tKFRuGUZhuA90OyIBpPCXkcbwU8DE= -github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4 h1:gQz4mCbXsO+nc9n1hCxHcGA3Zx3Eo+UHZoInFGUIXNM= github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= -github.com/prometheus/client_model v0.2.0 h1:uq5h0d+GuxiXLJLNABMgp2qUWDPiLvgCzz2dUR+/W/M= -github.com/prometheus/client_model v0.2.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= -github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= -github.com/prometheus/common v0.6.0 h1:kRhiuYSXR3+uv2IbVbZhUxK5zVD/2pp3Gd2PpvPkpEo= -github.com/prometheus/common v0.6.0/go.mod h1:eBmuwkDJBwy6iBfxCBob6t6dR6ENT/y+J+Zk0j9GMYc= -github.com/prometheus/common v0.9.1 h1:KOMtN28tlbam3/7ZKEYKHhKoJZYYj3gMH4uc62x7X7U= -github.com/prometheus/common v0.9.1/go.mod h1:yhUN8i9wzaXS3w1O07YhxHEBxD+W35wd8bs7vj7HSQ4= -github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= -github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= -github.com/prometheus/procfs v0.0.3 h1:CTwfnzjQ+8dS6MhHHu4YswVAD99sL2wjPqP+VkURmKE= -github.com/prometheus/procfs v0.0.3/go.mod h1:4A/X28fw3Fc593LaREMrKMqOKvUAntwMDaekg4FpcdQ= -github.com/prometheus/procfs v0.0.8/go.mod h1:7Qr8sr6344vo1JqZ6HhLceV9o3AJ1Ff+GxbHq6oeK9A= -github.com/prometheus/procfs v0.0.11 h1:DhHlBtkHWPYi8O2y31JkK0TF+DGM+51OopZjH/Ia5qI= -github.com/prometheus/procfs v0.0.11/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU= -github.com/rcrowley/go-metrics v0.0.0-20181016184325-3113b8401b8a/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4= -github.com/remyoudompheng/bigfft v0.0.0-20190728182440-6a916e37a237 h1:HQagqIiBmr8YXawX/le3+O26N+vPPC1PtjaF3mwnook= -github.com/remyoudompheng/bigfft v0.0.0-20190728182440-6a916e37a237/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= -github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg= -github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ= -github.com/rogpeppe/go-internal v1.1.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= -github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= -github.com/rogpeppe/go-internal v1.3.2/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc= -github.com/rogpeppe/go-internal v1.4.0 h1:LUa41nrWTQNGhzdsZ5lTnkwbNjj6rXTdazA1cSdjkOY= -github.com/rogpeppe/go-internal v1.4.0/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc= -github.com/rogpeppe/go-internal v1.5.0 h1:Usqs0/lDK/NqTkvrmKSwA/3XkZAs7ZAW/eLeQ2MVBTw= -github.com/rogpeppe/go-internal v1.5.0/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc= -github.com/russross/blackfriday v1.5.2 h1:HyvC0ARfnZBqnXwABFeSZHpKvJHJJfPz81GNueLj0oo= -github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g= -github.com/russross/blackfriday/v2 v2.0.1 h1:lPqVAte+HuHNfhJ/0LC98ESWRz8afy9tM/0RK8m9o+Q= -github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= -github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= -github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= -github.com/samuel/go-zookeeper v0.0.0-20180130194729-c4fab1ac1bec h1:6ncX5ko6B9LntYM0YBRXkiSaZMmLYeZ/NWcmeB43mMY= -github.com/samuel/go-zookeeper v0.0.0-20180130194729-c4fab1ac1bec/go.mod h1:gi+0XIa01GRL2eRQVjQkKGqKF3SF9vZR/HnPullcV2E= -github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0= -github.com/shirou/gopsutil v2.19.11+incompatible h1:lJHR0foqAjI4exXqWsU3DbH7bX1xvdhGdnXTIARA9W4= -github.com/shirou/gopsutil v2.19.11+incompatible/go.mod h1:5b4v6he4MtMOwMlS0TUMTu2PcXUg8+E1lC7eC3UO/RA= -github.com/shurcooL/sanitized_anchor_name v1.0.0 h1:PdmoCO6wvbs+7yrJyMORt4/BmY5IYyJwS/kOiWx8mHo= -github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= -github.com/siddontang/go v0.0.0-20180604090527-bdc77568d726 h1:xT+JlYxNGqyT+XcU8iUrN18JYed2TvG9yN5ULG2jATM= -github.com/siddontang/go v0.0.0-20180604090527-bdc77568d726/go.mod h1:3yhqj7WBBfRhbBlzyOC3gUxftwsU0u8gqevxwIHQpMw= -github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= -github.com/sirupsen/logrus v1.4.2 h1:SPIRibHv4MatM3XXNO2BJeFLZwZ2LvZgfQ5+UNI2im4= -github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= -github.com/sirupsen/logrus v1.5.0 h1:1N5EYkVAPEywqZRJd7cwnRtCb6xJx7NH3T3WUTF980Q= -github.com/sirupsen/logrus v1.5.0/go.mod h1:+F7Ogzej0PZc/94MaYx/nvG9jOFMD2osvC3s+Squfpo= -github.com/soheilhy/cmux v0.1.4 h1:0HKaf1o97UwFjHH9o5XsHUOF+tqmdA7KEzXLpiyaw0E= -github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM= -github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ= -github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= -github.com/spf13/cobra v0.0.3/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ= -github.com/spf13/cobra v0.0.5 h1:f0B+LkLX6DtmRH1isoNA9VTtNUK9K8xYd28JNNfOv/s= -github.com/spf13/cobra v0.0.5/go.mod h1:3K3wKZymM7VvHMDS9+Akkh4K60UwM26emMESw8tLCHU= -github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo= -github.com/spf13/pflag v1.0.1 h1:aCvUg6QPl3ibpQUxyLkrEkCHtPqYJL4x9AuhqVqFis4= -github.com/spf13/pflag v1.0.1/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= -github.com/spf13/pflag v1.0.3 h1:zPAT6CGy6wXeQ7NtTnaTerfKOsV6V6F8agHXFiazDkg= -github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= -github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= -github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= -github.com/spf13/viper v1.3.2/go.mod h1:ZiWeW+zYFKm7srdB9IoDzzZXaJaI5eL9QjNiN/DMA2s= -github.com/streadway/amqp v0.0.0-20190404075320-75d898a42a94/go.mod h1:AZpEONHx3DKn8O/DFsRAY58/XVQiIPMTMB1SddzLXVw= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/objx v0.1.1 h1:2vfRuCMp5sSVIDSqO8oNnWJq7mPa6KVP3iPIwFBuy8A= -github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= -github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= -github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk= -github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= -github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0= -github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/templexxx/cpufeat v0.0.0-20180724012125-cef66df7f161/go.mod h1:wM7WEvslTq+iOEAMDLSzhVuOt5BRZ05WirO+b09GHQU= -github.com/templexxx/xor v0.0.0-20181023030647-4e92f724b73b/go.mod h1:5XA7W9S6mni3h5uvOC75dA3m9CCCaS83lltmc0ukdi4= -github.com/tjfoc/gmsm v1.0.1/go.mod h1:XxO4hdhhrzAd+G4CjDqaOkd0hUzmtPR/d3EiBBMn/wc= -github.com/tmc/grpc-websocket-proxy v0.0.0-20170815181823-89b8d40f7ca8/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= -github.com/tmc/grpc-websocket-proxy v0.0.0-20200122045848-3419fae592fc h1:yUaosFVTJwnltaHbSNC3i82I92quFs+OFPRl8kNMVwo= -github.com/tmc/grpc-websocket-proxy v0.0.0-20200122045848-3419fae592fc/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= -github.com/tsuna/gohbase v0.0.0-20190502052937-24ffed0537aa h1:V/ABqiqsgqmpoIcLDSpJ1KqPfbxRmkcfET5+BRy9ctM= -github.com/tsuna/gohbase v0.0.0-20190502052937-24ffed0537aa/go.mod h1:3HfLQly3YNLGxNv/2YOfmz30vcjG9hbuME1GpxoLlGs= -github.com/uber/jaeger-client-go v2.25.0+incompatible h1:IxcNZ7WRY1Y3G4poYlx24szfsn/3LvK9QHCq9oQw8+U= -github.com/uber/jaeger-client-go v2.25.0+incompatible/go.mod h1:WVhlPFC8FDjOFMMWRy2pZqQJSXxYSwNYOkTr/Z6d3Kk= -github.com/uber/jaeger-lib v2.4.0+incompatible h1:fY7QsGQWiCt8pajv4r7JEvmATdCVaWxXbjwyYwsNaLQ= -github.com/uber/jaeger-lib v2.4.0+incompatible/go.mod h1:ComeNDZlWwrWnDv8aPp0Ba6+uUTzImX/AauajbLI56U= -github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0= -github.com/urfave/cli v1.20.0 h1:fDqGv3UG/4jbVl/QkFwEdddtEDjh/5Ov6X+0B/3bPaw= -github.com/urfave/cli v1.20.0/go.mod h1:70zkFmudgCuE/ngEzBv17Jvp/497gISqfk5gWijbERA= -github.com/urfave/cli/v2 v2.3.0 h1:qph92Y649prgesehzOrQjdWyxFOp/QVM+6imKHad91M= -github.com/urfave/cli/v2 v2.3.0/go.mod h1:LJmUH05zAU44vOAcrfzZQKsZbVcdbOG8rtL3/XcUArI= -github.com/xdg/scram v0.0.0-20180814205039-7eeb5667e42c/go.mod h1:lB8K/P019DLNhemzwFU4jHLhdvlE6uDZjXFejJXr49I= -github.com/xdg/stringprep v1.0.0/go.mod h1:Jhud4/sHMO4oL310DaZAKk9ZaJ08SJfe+sJh0HrGL1Y= -github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2 h1:eY9dn8+vbi4tKz5Qo6v2eYzo7kUS51QINcR5jNpbZS8= -github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU= -github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q= -github.com/xtaci/kcp-go v5.4.5+incompatible/go.mod h1:bN6vIwHQbfHaHtFpEssmWsN45a+AZwO7eyRCmEIbtvE= -github.com/xtaci/lossyconn v0.0.0-20190602105132-8df528c0c9ae/go.mod h1:gXtu8J62kEgmN++bm9BVICuT/e8yiLI2KFobd/TRFsE= -go.etcd.io/bbolt v1.3.4 h1:hi1bXHMVrlQh6WwxAy+qZCV/SYIlqo+Ushwdpa4tAKg= -go.etcd.io/bbolt v1.3.4/go.mod h1:G5EMThwa9y8QZGBClrRx5EY+Yw9kAhnjy3bSjsnlVTQ= -go.etcd.io/etcd v0.0.0-20200402134248-51bdeb39e698 h1:jWtjCJX1qxhHISBMLRztWwR+EXkI7MJAF2HjHAE/x/I= -go.etcd.io/etcd v0.0.0-20200402134248-51bdeb39e698/go.mod h1:YoUyTScD3Vcv2RBm3eGVOq7i1ULiz3OuXoQFWOirmAM= -go.uber.org/atomic v1.4.0 h1:cxzIVoETapQEqDhQu3QfnvXAV4AlzcvUCxkVUFw3+EU= -go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= -go.uber.org/atomic v1.6.0 h1:Ezj3JGmsOnG1MoRWQkPBsKLe9DwWD9QeXzTRzzldNVk= -go.uber.org/atomic v1.6.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ= -go.uber.org/multierr v1.1.0 h1:HoEmRHQPVSqub6w2z2d2EOVs2fjyFRGyofhKuyDq0QI= -go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0= -go.uber.org/multierr v1.5.0 h1:KCa4XfM8CWFCpxXRGok+Q0SS/0XBhMDbHHGABQLvD2A= -go.uber.org/multierr v1.5.0/go.mod h1:FeouvMocqHpRaaGuG9EjoKcStLC43Zu/fmqdUMPcKYU= -go.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee h1:0mgffUl7nfd+FpvXMVz4IDEaUSmT1ysygQC7qYo7sG4= -go.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee/go.mod h1:vJERXedbb3MVM5f9Ejo0C68/HhF8uaILCdgjnY+goOA= -go.uber.org/zap v1.10.0 h1:ORx85nbTijNz8ljznvCMR1ZBIPKFn3jQrag10X2AsuM= -go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= -go.uber.org/zap v1.14.1 h1:nYDKopTbvAPq/NrUVZwT15y2lpROBiLLyoRTbXOYWOo= -go.uber.org/zap v1.14.1/go.mod h1:Mb2vm2krFEG5DV0W9qcHBYFtp/Wku1cvYaqPsS/WYfc= -golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= -golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +github.com/stretchr/testify v1.5.1 h1:nOGnQDM7FYENwehXlg/kFVnos3rEvtKTjRvOWSzb6H4= +github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= -golang.org/x/crypto v0.0.0-20190404164418-38d8ce5564a5/go.mod h1:WFFai1msRO1wXaEeE5yQxYXgSfI8pQAWXbQop6sCtWE= -golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= -golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= -golang.org/x/crypto v0.0.0-20190621222207-cc06ce4a13d4/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= -golang.org/x/crypto v0.0.0-20191002192127-34f69633bfdc/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= -golang.org/x/crypto v0.0.0-20200323165209-0ec3e9974c59 h1:3zb4D3T4G8jdExgVU/95+vQXfpEPiMdCaZgmGVxjNHM= -golang.org/x/crypto v0.0.0-20200323165209-0ec3e9974c59/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= -golang.org/x/lint v0.0.0-20190930215403-16217165b5de h1:5hukYrvBGR8/eNkX5mdUezrA6JiaEZDtJb9Ei+1LlBs= -golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= -golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190311183353-d8887717615a h1:oWX7TPOiFAMXLq8o0ikBYfCJVlRHBcsciT5bXOrH628= golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= -golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20190813141303-74dc4d7220e7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20190912160710-24e19bdeb0f2 h1:4dVFTC832rPn4pomLSz1vA+are2+dU19w1H8OngV7nc= -golang.org/x/net v0.0.0-20190912160710-24e19bdeb0f2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20191002035440-2ec189313ef0/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e h1:3G+cUijn7XD+S4eJFddp53Pv7+slrESplyjG25HgL+k= -golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190423024810-112230192c58 h1:8gQV6CLnAEikrhgkHFbMAEhagSSnXWGV915qUMm9mrU= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e h1:vcxGaoTs7kV8m5Np9uUNQin4BrLOthgV7252N8V+FwY= -golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20181107165924-66b7b1311ac8/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20181205085412-a5c9d58dba9a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a h1:1BGLXjeY4akVXGgbC9HugT3Jv3hCI0z56oJR5vAMgBU= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20190403152447-81d4e9dc473e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190515120540-06a5c4944438/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190801041406-cbf593c0f2f3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190912141932-bc967efca4b8 h1:41hwlulw1prEMBxLQSlMSux1zxJf07B3WPsdjJlKZxE= -golang.org/x/sys v0.0.0-20190912141932-bc967efca4b8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200106162015-b016eb3dc98e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20201202213521-69691e467435 h1:25AvDqqB9PrNqj1FLf2/70I4W0L19qqoaFq3gjNwbKk= -golang.org/x/sys v0.0.0-20201202213521-69691e467435/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9 h1:L2auWcuQIvxz9xSEqzESnV/QN/gNRXNApHi3fYwl2w0= +golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= -golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs= -golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= -golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= -golang.org/x/time v0.0.0-20190308202827-9d24e82272b4 h1:SvFZT6jyqRaOeXpc5h/JSfZenJ2O330aBsf7JfSUXmQ= -golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= -golang.org/x/time v0.0.0-20191024005414-555d28b269f0 h1:/5xXl8Y5W96D+TtHSlonuFqGHIWVuyCkGJLwGh9JJFs= -golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= -golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -golang.org/x/tools v0.0.0-20181030221726-6c7e314b6563/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= -golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= -golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b h1:mSUCVIwDx4hfXJfWsOPfdzEHxzb2Xjl6BQ8YgPnazQA= -golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= -golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= -golang.org/x/tools v0.0.0-20190912185636-87d9f09c5d89 h1:WiVZGyzQN7gPNLRkkpsNX3jC0Jx5j9GxadCZW/8eXw0= -golang.org/x/tools v0.0.0-20190912185636-87d9f09c5d89/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191004055002-72853e10c5a3 h1:2AmBLzhAfXj+2HCW09VCkJtHIYgHTIPcTeYqgP7Bwt0= -golang.org/x/tools v0.0.0-20191004055002-72853e10c5a3/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191029041327-9cc4af7d6b2c/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191029190741-b9c20aec41a5/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191105231337-689d0f08e67a h1:RzzIfXstYPS78k0QViPGpDcTlV+QuYrbxVmsxDHdxTs= -golang.org/x/tools v0.0.0-20191105231337-689d0f08e67a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE= -golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= -google.golang.org/appengine v1.6.1 h1:QzqyMA1tlu6CgqCDUtU9V+ZKhLFT2dkJuANu5QaxI3I= -google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0= google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= -google.golang.org/genproto v0.0.0-20190927181202-20e1ac93f88c/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8= -google.golang.org/genproto v0.0.0-20200402124713-8ff61da6d932 h1:aw1IXx+GKsPxp8MaZuDaKwNdOno9liI4TElk87LJFAo= -google.golang.org/genproto v0.0.0-20200402124713-8ff61da6d932/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013 h1:+kGHl1aib/qcwaRi1CbqBZ1rk19r85MNUf8HaBghugY= +google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= +google.golang.org/genproto v0.0.0-20210114201628-6edceaf6022f h1:izedQ6yVIc5mZsRuXzmSreCOlzI0lCU1HpG8yEdMiKw= +google.golang.org/genproto v0.0.0-20210114201628-6edceaf6022f/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= -google.golang.org/grpc v1.20.0/go.mod h1:chYK+tFQF0nDUGJgXMSgLCQk3phJEuONr2DCgLDdAQM= google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= -google.golang.org/grpc v1.23.1 h1:q4XQuHFC6I28BKZpo6IYyb3mNO+l7lSOxRuYTCiDfXk= -google.golang.org/grpc v1.23.1/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= -google.golang.org/grpc v1.24.0 h1:vb/1TCsVn3DcJlQ0Gs1yB1pKI6Do2/QNwxdKqmc/b0s= -google.golang.org/grpc v1.24.0/go.mod h1:XDChyiUovWa60DnaeDeZmSW86xtLtjtZbwvSiRnRtcA= google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= -google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= -google.golang.org/grpc v1.29.1 h1:EC2SB8S04d2r73uptxphDSUG+kTKVgjRPF+N3xpxRB4= -google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk= -gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= -gopkg.in/bsm/ratelimit.v1 v1.0.0-20160220154919-db14e161995a/go.mod h1:KF9sEfUPAXdG8Oev9e99iLGnl2uJMjc5B+4y3O7x610= +google.golang.org/grpc v1.35.0 h1:TwIQcH3es+MojMVojxxfQ3l3OF2KzlRxML2xZq0kRo8= +google.golang.org/grpc v1.35.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= +google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= +google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= +google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= +google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= +google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= +google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4= +google.golang.org/protobuf v1.25.0 h1:Ejskq+SyPohKW+1uil0JJMtmHCgJPJ/qWTxr8qp+R4c= +google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY= -gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo= -gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f h1:BLraFXnmrev5lT+xlilqcH8XK9/i0At2xKjWk4p6zsU= -gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/cheggaaa/pb.v1 v1.0.25/go.mod h1:V/YB90LKu/1FcN3WVnfiiE5oMCibMjukxqG/qStrOgw= -gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= -gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= -gopkg.in/go-playground/assert.v1 v1.2.1 h1:xoYuJVE7KT85PYWrN730RguIQO0ePzVRfFMXadIrXTM= -gopkg.in/go-playground/assert.v1 v1.2.1/go.mod h1:9RXL0bg/zibRAgZUYszZSwO/z8Y/a8bDuhia5mkpMnE= -gopkg.in/go-playground/validator.v9 v9.29.1 h1:SvGtYmN60a5CVKTOzMSyfzWDeZRxRuGvRQyEAKbw1xc= -gopkg.in/go-playground/validator.v9 v9.29.1/go.mod h1:+c9/zcJMFNgbLvly1L1V+PpxWdVbfP1avr/N00E2vyQ= -gopkg.in/jcmturner/aescts.v1 v1.0.1/go.mod h1:nsR8qBOg+OucoIW+WMhB3GspUQXq9XorLnQb9XtvcOo= -gopkg.in/jcmturner/dnsutils.v1 v1.0.1/go.mod h1:m3v+5svpVOhtFAP/wSz+yzh4Mc0Fg7eRhxkJMWSIz9Q= -gopkg.in/jcmturner/goidentity.v3 v3.0.0/go.mod h1:oG2kH0IvSYNIu80dVAyu/yoefjq1mNfM5bm88whjWx4= -gopkg.in/jcmturner/gokrb5.v7 v7.2.3/go.mod h1:l8VISx+WGYp+Fp7KRbsiUuXTTOnxIc3Tuvyavf11/WM= -gopkg.in/jcmturner/rpc.v1 v1.1.0/go.mod h1:YIdkC4XfD6GXbzje11McwsDuOlZQSb9W4vfLvuNnlv8= -gopkg.in/redis.v4 v4.2.4/go.mod h1:8KREHdypkCEojGKQcjMqAODMICIVwZAONWq8RowTITA= -gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo= -gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= -gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74= -gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.2.5/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10= -gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= -gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= -gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= -gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= -honnef.co/go/tools v0.0.1-2019.2.3 h1:3JgtbtFHMiCmsznwGVTUWbgGov+pVqnlf1dEJTNAXeM= -honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= -sigs.k8s.io/yaml v1.1.0 h1:4A07+ZFc2wgJwo8YNlQpr1rVlgUDlxXHhPJciaPY5gs= -sigs.k8s.io/yaml v1.1.0/go.mod h1:UJmg0vDUVViEyp3mgSv9WPwZCDxu4rQW1olrI1uml+o= -sigs.k8s.io/yaml v1.2.0 h1:kr/MCeFWJWTwyaHoR9c8EjH9OumOmoF9YGiZd7lFm/Q= -sigs.k8s.io/yaml v1.2.0/go.mod h1:yfXDCHCao9+ENCvLSE62v9VSji2MKu5jeNfTrofGhJc= diff --git a/internal/README.md b/internal/README.md new file mode 100644 index 000000000..a57dcfad0 --- /dev/null +++ b/internal/README.md @@ -0,0 +1 @@ +# internal diff --git a/internal/host/host.go b/internal/host/host.go new file mode 100644 index 000000000..3ec498e05 --- /dev/null +++ b/internal/host/host.go @@ -0,0 +1,72 @@ +package host + +import ( + "fmt" + "net" + "strconv" +) + +var ( + privateAddrs = []string{"10.0.0.0/8", "172.16.0.0/12", "192.168.0.0/16", "100.64.0.0/10", "fd00::/8"} +) + +func isPrivateIP(addr string) bool { + ipAddr := net.ParseIP(addr) + for _, privateAddr := range privateAddrs { + if _, priv, err := net.ParseCIDR(privateAddr); err == nil { + if priv.Contains(ipAddr) { + return true + } + } + } + return false +} + +// Port return a real port. +func Port(lis net.Listener) (int, bool) { + if addr, ok := lis.Addr().(*net.TCPAddr); ok { + return addr.Port, true + } + return 0, false +} + +// Extract returns a private addr and port. +func Extract(hostport string, lis net.Listener) (string, error) { + addr, port, err := net.SplitHostPort(hostport) + if err != nil { + return "", err + } + if lis != nil { + if p, ok := Port(lis); ok { + port = strconv.Itoa(p) + } + } + if len(addr) > 0 && (addr != "0.0.0.0" && addr != "[::]" && addr != "::") { + return net.JoinHostPort(addr, port), nil + } + ifaces, err := net.Interfaces() + if err != nil { + return "", fmt.Errorf("Failed to get net interfaces: %v", err) + } + for _, iface := range ifaces { + addrs, err := iface.Addrs() + if err != nil { + continue + } + for _, rawAddr := range addrs { + var ip net.IP + switch addr := rawAddr.(type) { + case *net.IPAddr: + ip = addr.IP + case *net.IPNet: + ip = addr.IP + default: + continue + } + if isPrivateIP(ip.String()) { + return net.JoinHostPort(ip.String(), port), nil + } + } + } + return "", nil +} diff --git a/internal/host/host_test.go b/internal/host/host_test.go new file mode 100644 index 000000000..900f3ad0d --- /dev/null +++ b/internal/host/host_test.go @@ -0,0 +1,63 @@ +package host + +import ( + "net" + "testing" +) + +func TestPrivateIP(t *testing.T) { + tests := []struct { + addr string + expect bool + }{ + {"10.1.0.1", true}, + {"172.16.0.1", true}, + {"192.168.1.1", true}, + {"8.8.8.8", false}, + } + for _, test := range tests { + t.Run(test.addr, func(t *testing.T) { + res := isPrivateIP(test.addr) + if res != test.expect { + t.Fatalf("expected %t got %t", test.expect, res) + } + }) + } +} + +func TestExtract(t *testing.T) { + tests := []struct { + addr string + expect string + }{ + {"127.0.0.1:80", "127.0.0.1:80"}, + {"10.0.0.1:80", "10.0.0.1:80"}, + {"172.16.0.1:80", "172.16.0.1:80"}, + {"192.168.1.1:80", "192.168.1.1:80"}, + {"0.0.0.0:80", ""}, + {"[::]:80", ""}, + {":80", ""}, + } + for _, test := range tests { + t.Run(test.addr, func(t *testing.T) { + res, err := Extract(test.addr, nil) + if err != nil { + t.Fatal(err) + } + if res != test.expect && (test.expect == "" && test.addr == test.expect) { + t.Fatalf("expected %s got %s", test.expect, res) + } + }) + } +} + +func TestPort(t *testing.T) { + lis, err := net.Listen("tcp", ":0") + if err != nil { + t.Fatal(err) + } + port, ok := Port(lis) + if !ok || port == 0 { + t.Fatalf("expected: %s got %d", lis.Addr().String(), port) + } +} diff --git a/log/README.md b/log/README.md new file mode 100644 index 000000000..80758192e --- /dev/null +++ b/log/README.md @@ -0,0 +1,15 @@ +# Log + +## Usage + +### Structured logging + +``` +logger := stdlog.NewLogger(stdlog.Writer(os.Stdout)) +log := log.NewHelper("module_name", logger) +// Levels +log.Info("some log") +log.Infof("format %s", "some log") +log.Infow("field_name", "some log") +``` + diff --git a/log/helper.go b/log/helper.go new file mode 100644 index 000000000..24aa16cba --- /dev/null +++ b/log/helper.go @@ -0,0 +1,86 @@ +package log + +import ( + "fmt" +) + +var nop Logger = new(nopLogger) + +// Helper is a logger helper. +type Helper struct { + debug Logger + info Logger + warn Logger + err Logger +} + +// NewHelper new a logger helper. +func NewHelper(name string, logger Logger) *Helper { + log := With(logger, "module", name) + return &Helper{ + debug: Debug(log), + info: Info(log), + warn: Warn(log), + err: Error(log), + } +} + +// Debug logs a message at debug level. +func (h *Helper) Debug(a ...interface{}) { + h.debug.Print("message", fmt.Sprint(a...)) +} + +// Debugf logs a message at debug level. +func (h *Helper) Debugf(format string, a ...interface{}) { + h.debug.Print("message", fmt.Sprintf(format, a...)) +} + +// Debugw logs a message at debug level. +func (h *Helper) Debugw(kvpair ...interface{}) { + h.debug.Print(kvpair...) +} + +// Info logs a message at info level. +func (h *Helper) Info(a ...interface{}) { + h.info.Print("message", fmt.Sprint(a...)) +} + +// Infof logs a message at info level. +func (h *Helper) Infof(format string, a ...interface{}) { + h.info.Print("message", fmt.Sprintf(format, a...)) +} + +// Infow logs a message at info level. +func (h *Helper) Infow(kvpair ...interface{}) { + h.info.Print(kvpair...) +} + +// Warn logs a message at warn level. +func (h *Helper) Warn(a ...interface{}) { + h.warn.Print("message", fmt.Sprint(a...)) +} + +// Warnf logs a message at warnf level. +func (h *Helper) Warnf(format string, a ...interface{}) { + h.warn.Print("message", fmt.Sprintf(format, a...)) +} + +// Warnw logs a message at warnf level. +func (h *Helper) Warnw(kvpair ...interface{}) { + h.warn.Print(kvpair...) +} + +// Error logs a message at error level. +func (h *Helper) Error(a ...interface{}) { + h.err.Print("message", fmt.Sprint(a...)) +} + +// Errorf logs a message at error level. +func (h *Helper) Errorf(format string, a ...interface{}) { + h.err.Print("message", fmt.Sprintf(format, a...)) +} + +// Errorw logs a message at error level. +func (h *Helper) Errorw(kvpair ...interface{}) { + h.err.Print(kvpair...) +} diff --git a/log/helper_test.go b/log/helper_test.go new file mode 100644 index 000000000..51c45ad41 --- /dev/null +++ b/log/helper_test.go @@ -0,0 +1,20 @@ +package log + +import ( + "testing" +) + +func TestHelper(t *testing.T) { + log := NewHelper("test", DefaultLogger) + log.Debug("test debug") + log.Debugf("test %s", "debug") + log.Debugw("log", "test debug") +} + +func TestHelperLevel(t *testing.T) { + log := NewHelper("test", DefaultLogger) + log.Debug("test debug") + log.Info("test info") + log.Warn("test warn") + log.Error("test error") +} diff --git a/log/level.go b/log/level.go new file mode 100644 index 000000000..dcd26fef9 --- /dev/null +++ b/log/level.go @@ -0,0 +1,40 @@ +package log + +// Level is a logger level. +type Level int8 + +const ( + // LevelDebug is logger debug level. + LevelDebug Level = iota + // LevelInfo is logger info level. + LevelInfo + // LevelWarn is logger warn level. + LevelWarn + // LevelError is logger error level. + LevelError +) + +const ( + // LevelKey is logger level key. + LevelKey = "level" +) + +// Enabled compare whether the logging level is enabled. +func (l Level) Enabled(lv Level) bool { + return lv >= l +} + +func (l Level) String() string { + switch l { + case LevelDebug: + return "DEBUG" + case LevelInfo: + return "INFO" + case LevelWarn: + return "WARN" + case LevelError: + return "ERROR" + default: + return "" + } +} diff --git a/log/log.go b/log/log.go new file mode 100644 index 000000000..3884b6b54 --- /dev/null +++ b/log/log.go @@ -0,0 +1,48 @@ +package log + +var ( + // DefaultLogger is default logger. + DefaultLogger Logger = NewStdLogger() +) + +// Logger is a logger interface. +type Logger interface { + Print(pairs ...interface{}) +} + +type logger struct { + log Logger + pairs []interface{} +} + +func (l *logger) Print(pairs ...interface{}) { + l.log.Print(append(pairs, l.pairs...)...) +} + +// With with logger kv pairs. +func With(log Logger, pairs ...interface{}) Logger { + if len(pairs) == 0 { + return log + } + return &logger{log: log, pairs: pairs} +} + +// Debug returns a debug logger. +func Debug(log Logger) Logger { + return With(log, LevelKey, LevelDebug) +} + +// Info returns a info logger. +func Info(log Logger) Logger { + return With(log, LevelKey, LevelInfo) +} + +// Warn return a warn logger. +func Warn(log Logger) Logger { + return With(log, LevelKey, LevelWarn) +} + +// Error returns a error logger. +func Error(log Logger) Logger { + return With(log, LevelKey, LevelError) +} diff --git a/log/log_test.go b/log/log_test.go new file mode 100644 index 000000000..d99fc121c --- /dev/null +++ b/log/log_test.go @@ -0,0 +1,13 @@ +package log + +import ( + "testing" +) + +func TestLogger(t *testing.T) { + logger := NewStdLogger() + Debug(logger).Print("log", "test debug") + Info(logger).Print("log", "test info") + Warn(logger).Print("log", "test warn") + Error(logger).Print("log", "test error") +} diff --git a/log/nop.go b/log/nop.go new file mode 100644 index 000000000..bca1f0a53 --- /dev/null +++ b/log/nop.go @@ -0,0 +1,5 @@ +package log + +type nopLogger struct{} + +func (l *nopLogger) Print(kvpair ...interface{}) {} diff --git a/log/std.go b/log/std.go new file mode 100644 index 000000000..e5eaf6ffb --- /dev/null +++ b/log/std.go @@ -0,0 +1,45 @@ +package log + +import ( + "bytes" + "fmt" + "log" + "os" + "sync" +) + +var _ Logger = (*stdLogger)(nil) + +type stdLogger struct { + log *log.Logger + pool *sync.Pool +} + +// NewStdLogger new a std logger with options. +func NewStdLogger() Logger { + return &stdLogger{ + log: log.New(os.Stderr, "", log.LstdFlags), + pool: &sync.Pool{ + New: func() interface{} { + return new(bytes.Buffer) + }, + }, + } +} + +// Print print the kv pairs log. +func (s *stdLogger) Print(kvpair ...interface{}) { + if len(kvpair) == 0 { + return + } + if len(kvpair)%2 != 0 { + kvpair = append(kvpair, "") + } + buf := s.pool.Get().(*bytes.Buffer) + for i := 0; i < len(kvpair); i += 2 { + fmt.Fprintf(buf, "%s=%v ", kvpair[i], kvpair[i+1]) + } + s.log.Println(buf.String()) + buf.Reset() + s.pool.Put(buf) +} diff --git a/log/std_test.go b/log/std_test.go new file mode 100644 index 000000000..60975856a --- /dev/null +++ b/log/std_test.go @@ -0,0 +1,12 @@ +package log + +import "testing" + +func TestFmtLogger(t *testing.T) { + logger := NewStdLogger() + + Debug(logger).Print("log", "test debug") + Info(logger).Print("log", "test info") + Warn(logger).Print("log", "test warn") + Error(logger).Print("log", "test error") +} diff --git a/metrics/README.md b/metrics/README.md new file mode 100644 index 000000000..f36beb4ed --- /dev/null +++ b/metrics/README.md @@ -0,0 +1,2 @@ +# Metrics +* [Prometheus](https://github.com/go-kratos/prometheus) diff --git a/metrics/metrics.go b/metrics/metrics.go new file mode 100644 index 000000000..4e221eb7a --- /dev/null +++ b/metrics/metrics.go @@ -0,0 +1,22 @@ +package metrics + +// Counter is metrics counter. +type Counter interface { + With(lvs ...string) Counter + Inc() + Add(delta float64) +} + +// Gauge is metrics gauge. +type Gauge interface { + With(lvs ...string) Gauge + Set(value float64) + Add(delta float64) + Sub(delta float64) +} + +// Observer is metrics observer. +type Observer interface { + With(lvs ...string) Observer + Observe(float64) +} diff --git a/middleware/logging/logging.go b/middleware/logging/logging.go new file mode 100644 index 000000000..2486b0603 --- /dev/null +++ b/middleware/logging/logging.go @@ -0,0 +1,118 @@ +package logging + +import ( + "context" + + "github.com/go-kratos/kratos/v2/errors" + "github.com/go-kratos/kratos/v2/log" + "github.com/go-kratos/kratos/v2/middleware" + "github.com/go-kratos/kratos/v2/transport/grpc" + "github.com/go-kratos/kratos/v2/transport/http" +) + +// Option is HTTP logging option. +type Option func(*options) + +type options struct { + logger log.Logger +} + +// WithLogger with middleware logger. +func WithLogger(logger log.Logger) Option { + return func(o *options) { + o.logger = logger + } +} + +// Server is an server logging middleware. +func Server(opts ...Option) middleware.Middleware { + options := options{ + logger: log.DefaultLogger, + } + for _, o := range opts { + o(&options) + } + log := log.NewHelper("middleware/logging", options.logger) + return func(handler middleware.Handler) middleware.Handler { + return func(ctx context.Context, req interface{}) (interface{}, error) { + var ( + path string + method string + ) + if info, ok := http.FromServerContext(ctx); ok { + path = info.Request.RequestURI + method = info.Request.Method + } else if info, ok := grpc.FromServerContext(ctx); ok { + path = info.FullMethod + method = "POST" + } + reply, err := handler(ctx, req) + if err != nil { + log.Errorw( + "kind", "server", + "path", path, + "method", method, + "code", errors.Code(err), + "error", err.Error(), + ) + return nil, err + } + log.Infow( + "kind", "server", + "path", path, + "method", method, + "code", 0, + ) + return reply, nil + } + } +} + +// Client is an client logging middleware. +func Client(opts ...Option) middleware.Middleware { + options := options{ + logger: log.DefaultLogger, + } + for _, o := range opts { + o(&options) + } + log := log.NewHelper("middleware/logging", options.logger) + return func(handler middleware.Handler) middleware.Handler { + return func(ctx context.Context, req interface{}) (interface{}, error) { + var ( + component string + path string + method string + ) + if info, ok := http.FromClientContext(ctx); ok { + component = "HTTP" + path = info.Request.RequestURI + method = info.Request.Method + } else if info, ok := grpc.FromClientContext(ctx); ok { + component = "gRPC" + path = info.FullMethod + method = "POST" + } + reply, err := handler(ctx, req) + if err != nil { + log.Errorw( + "kind", "client", + "component", component, + "path", path, + "method", method, + "code", errors.Code(err), + "error", err.Error(), + ) + return nil, err + } + log.Infow( + "kind", "client", + "component", component, + "path", path, + "method", method, + "code", 0, + ) + return reply, nil + } + } +} diff --git a/middleware/middleware.go b/middleware/middleware.go new file mode 100644 index 000000000..8d687c0f7 --- /dev/null +++ b/middleware/middleware.go @@ -0,0 +1,21 @@ +package middleware + +import ( + "context" +) + +// Handler defines the handler invoked by Middleware. +type Handler func(ctx context.Context, req interface{}) (interface{}, error) + +// Middleware is HTTP/gRPC transport middleware. +type Middleware func(Handler) Handler + +// Chain returns a Middleware that specifies the chained handler for endpoint. +func Chain(outer Middleware, others ...Middleware) Middleware { + return func(next Handler) Handler { + for i := len(others) - 1; i >= 0; i-- { + next = others[i](next) + } + return outer(next) + } +} diff --git a/middleware/recovery/recovery.go b/middleware/recovery/recovery.go new file mode 100644 index 000000000..940d2fc5d --- /dev/null +++ b/middleware/recovery/recovery.go @@ -0,0 +1,64 @@ +package recovery + +import ( + "context" + "runtime" + + "github.com/go-kratos/kratos/v2/errors" + "github.com/go-kratos/kratos/v2/log" + "github.com/go-kratos/kratos/v2/middleware" +) + +// HandlerFunc is recovery handler func. +type HandlerFunc func(ctx context.Context, req, err interface{}) error + +// Option is recovery option. +type Option func(*options) + +type options struct { + handler HandlerFunc + logger log.Logger +} + +// WithHandler with recovery handler. +func WithHandler(h HandlerFunc) Option { + return func(o *options) { + o.handler = h + } +} + +// WithLogger with recovery logger. +func WithLogger(logger log.Logger) Option { + return func(o *options) { + o.logger = logger + } +} + +// Recovery is a server middleware that recovers from any panics. +func Recovery(opts ...Option) middleware.Middleware { + options := options{ + logger: log.DefaultLogger, + handler: func(ctx context.Context, req, err interface{}) error { + return errors.Unknown("Unknown", "panic triggered: %v", err) + }, + } + for _, o := range opts { + o(&options) + } + log := log.NewHelper("middleware/recovery", log.DefaultLogger) + return func(handler middleware.Handler) middleware.Handler { + return func(ctx context.Context, req interface{}) (reply interface{}, err error) { + defer func() { + if rerr := recover(); rerr != nil { + buf := make([]byte, 64<<10) + n := runtime.Stack(buf, false) + buf = buf[:n] + log.Errorf("%v: %+v\n%s\n", rerr, req, buf) + + err = options.handler(ctx, req, rerr) + } + }() + return handler(ctx, req) + } + } +} diff --git a/middleware/status/status.go b/middleware/status/status.go new file mode 100644 index 000000000..29fb67e99 --- /dev/null +++ b/middleware/status/status.go @@ -0,0 +1,113 @@ +package status + +import ( + "context" + + "github.com/go-kratos/kratos/v2/errors" + "github.com/go-kratos/kratos/v2/middleware" + "github.com/golang/protobuf/proto" + "github.com/golang/protobuf/ptypes" + "google.golang.org/genproto/googleapis/rpc/errdetails" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" +) + +// HandlerFunc is middleware error handler. +type HandlerFunc func(error) error + +// Option is recovery option. +type Option func(*options) + +type options struct { + handler HandlerFunc +} + +// WithHandler with status handler. +func WithHandler(h HandlerFunc) Option { + return func(o *options) { + o.handler = h + } +} + +// Server is an error middleware. +func Server(opts ...Option) middleware.Middleware { + options := options{ + handler: errorEncode, + } + for _, o := range opts { + o(&options) + } + return func(handler middleware.Handler) middleware.Handler { + return func(ctx context.Context, req interface{}) (interface{}, error) { + reply, err := handler(ctx, req) + if err != nil { + return nil, options.handler(err) + } + return reply, nil + } + } +} + +// Client is an error middleware. +func Client(opts ...Option) middleware.Middleware { + options := options{ + handler: errorDecode, + } + for _, o := range opts { + o(&options) + } + return func(handler middleware.Handler) middleware.Handler { + return func(ctx context.Context, req interface{}) (interface{}, error) { + reply, err := handler(ctx, req) + if err != nil { + return nil, options.handler(err) + } + return reply, nil + } + } +} + +func errorEncode(err error) error { + se, ok := errors.FromError(err) + if !ok { + se = &errors.StatusError{ + Code: 2, + } + } + gs := status.Newf(codes.Code(se.Code), "%s: %s", se.Reason, se.Message) + details := []proto.Message{ + &errdetails.ErrorInfo{ + Reason: se.Reason, + Metadata: map[string]string{"message": se.Message}, + }, + } + for _, any := range se.Details { + detail := &ptypes.DynamicAny{} + if err := ptypes.UnmarshalAny(any, detail); err != nil { + continue + } + details = append(details, detail.Message) + } + gs, err = gs.WithDetails(details...) + if err != nil { + return err + } + return gs.Err() +} + +func errorDecode(err error) error { + gs := status.Convert(err) + se := &errors.StatusError{ + Code: int32(gs.Code()), + Details: gs.Proto().Details, + } + for _, detail := range gs.Details() { + switch d := detail.(type) { + case *errdetails.ErrorInfo: + se.Reason = d.Reason + se.Message = d.Metadata["message"] + return se + } + } + return se +} diff --git a/middleware/status/status_test.go b/middleware/status/status_test.go new file mode 100644 index 000000000..f6636bba9 --- /dev/null +++ b/middleware/status/status_test.go @@ -0,0 +1,21 @@ +package status + +import ( + "testing" + + "github.com/go-kratos/kratos/v2/errors" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" +) + +func TestErrEncoder(t *testing.T) { + err := errors.InvalidArgument("InvalidArgument", "format") + en := errorEncode(err) + if code := status.Code(en); code != codes.InvalidArgument { + t.Errorf("expected %d got %d", codes.InvalidArgument, code) + } + de := errorDecode(en) + if !errors.IsInvalidArgument(de) { + t.Errorf("expected %v got %v", err, de) + } +} diff --git a/middleware/tracing/tracing.go b/middleware/tracing/tracing.go new file mode 100644 index 000000000..bf4779ae6 --- /dev/null +++ b/middleware/tracing/tracing.go @@ -0,0 +1,128 @@ +package tracing + +import ( + "context" + + "github.com/go-kratos/kratos/v2/middleware" + "github.com/go-kratos/kratos/v2/transport/grpc" + "github.com/go-kratos/kratos/v2/transport/http" + "github.com/opentracing/opentracing-go" + "github.com/opentracing/opentracing-go/ext" + "github.com/opentracing/opentracing-go/log" + "google.golang.org/grpc/metadata" +) + +// Option is tracing option. +type Option func(*options) + +type options struct { + tracer opentracing.Tracer +} + +// WithTracer sets a custom tracer to be used for this middleware, otherwise the opentracing.GlobalTracer is used. +func WithTracer(tracer opentracing.Tracer) Option { + return func(o *options) { + o.tracer = tracer + } +} + +// Server returns a new server middleware for OpenTracing. +func Server(opts ...Option) middleware.Middleware { + options := options{ + tracer: opentracing.GlobalTracer(), + } + for _, o := range opts { + o(&options) + } + return func(handler middleware.Handler) middleware.Handler { + return func(ctx context.Context, req interface{}) (reply interface{}, err error) { + var ( + component string + operation string + spanContext opentracing.SpanContext + ) + if info, ok := http.FromServerContext(ctx); ok { + // HTTP span + component = "HTTP" + operation = info.Request.RequestURI + spanContext, _ = options.tracer.Extract( + opentracing.HTTPHeaders, + opentracing.HTTPHeadersCarrier(info.Request.Header), + ) + } else if info, ok := grpc.FromServerContext(ctx); ok { + // gRPC span + component = "gRPC" + operation = info.FullMethod + if md, ok := metadata.FromIncomingContext(ctx); ok { + spanContext, _ = options.tracer.Extract( + opentracing.HTTPHeaders, + opentracing.HTTPHeadersCarrier(md), + ) + } + } + span := options.tracer.StartSpan( + operation, + ext.RPCServerOption(spanContext), + opentracing.Tag{Key: string(ext.Component), Value: component}, + ) + defer span.Finish() + if reply, err = handler(ctx, req); err != nil { + ext.Error.Set(span, true) + span.LogFields( + log.String("event", "error"), + log.String("message", err.Error()), + ) + } + return + } + } +} + +// Client returns a new client middleware for OpenTracing. +func Client(opts ...Option) middleware.Middleware { + options := options{} + for _, o := range opts { + o(&options) + } + return func(handler middleware.Handler) middleware.Handler { + return func(ctx context.Context, req interface{}) (reply interface{}, err error) { + var ( + operation string + parent opentracing.SpanContext + carrier opentracing.HTTPHeadersCarrier + ) + if span := opentracing.SpanFromContext(ctx); span != nil { + parent = span.Context() + } + if info, ok := http.FromClientContext(ctx); ok { + // HTTP span + operation = info.Request.RequestURI + carrier = opentracing.HTTPHeadersCarrier(info.Request.Header) + } else if info, ok := grpc.FromClientContext(ctx); ok { + // gRPC span + operation = info.FullMethod + if md, ok := metadata.FromOutgoingContext(ctx); ok { + carrier = opentracing.HTTPHeadersCarrier(md) + ctx = metadata.NewOutgoingContext(ctx, md) + } + } + span := options.tracer.StartSpan( + operation, + opentracing.ChildOf(parent), + ext.SpanKindRPCClient, + ) + defer span.Finish() + options.tracer.Inject(span.Context(), opentracing.HTTPHeaders, carrier) + ctx = opentracing.ContextWithSpan(ctx, span) + // send handler + if reply, err = handler(ctx, req); err != nil { + ext.Error.Set(span, true) + span.LogFields( + log.String("event", "error"), + log.String("message", err.Error()), + ) + } + return + } + } +} diff --git a/misc/stat/dashboard/README.md b/misc/stat/dashboard/README.md deleted file mode 100644 index f6826f55b..000000000 --- a/misc/stat/dashboard/README.md +++ /dev/null @@ -1,15 +0,0 @@ -#### dashboard - -> 监控模版,针对服务框架内的监控指标的UI展示。 - -##### Requirments - -- [Grafana](https://grafana.com) >= v6.1.4 -- [Prometheus](https://prometheus.io) >= 2.x - -##### Quick start - -1. 搭建grafana -2. 导入`prometheus.json`文件 -3. 修改对应的`Data source` -4. 保存 diff --git a/misc/stat/dashboard/prometheus.json b/misc/stat/dashboard/prometheus.json deleted file mode 100644 index d1529a8a7..000000000 --- a/misc/stat/dashboard/prometheus.json +++ /dev/null @@ -1,5917 +0,0 @@ -{ - "annotations": { - "list": [ - { - "builtIn": 1, - "datasource": "-- Grafana --", - "enable": true, - "hide": true, - "iconColor": "rgba(0, 211, 255, 1)", - "name": "Annotations & Alerts", - "type": "dashboard" - } - ] - }, - "editable": true, - "gnetId": null, - "graphTooltip": 0, - "id": 632, - "iteration": 1564558933838, - "links": [], - "panels": [ - { - "cacheTimeout": null, - "colorBackground": false, - "colorValue": true, - "colors": [ - "rgba(245, 54, 54, 0.9)", - "rgba(40, 237, 51, 0.89)", - "rgba(242, 18, 18, 0.97)" - ], - "datasource": "$datasource", - "decimals": 1, - "format": "s", - "gauge": { - "maxValue": 100, - "minValue": 0, - "show": false, - "thresholdLabels": false, - "thresholdMarkers": true - }, - "gridPos": { - "h": 3, - "w": 4, - "x": 0, - "y": 0 - }, - "hideTimeOverride": false, - "id": 78, - "interval": null, - "links": [], - "mappingType": 1, - "mappingTypes": [ - { - "name": "value to text", - "value": 1 - }, - { - "name": "range to text", - "value": 2 - } - ], - "maxDataPoints": 100, - "nullPointMode": "connected", - "nullText": null, - "postfix": "", - "postfixFontSize": "50%", - "prefix": "", - "prefixFontSize": "50%", - "rangeMaps": [ - { - "from": "null", - "text": "N/A", - "to": "null" - } - ], - "sparkline": { - "fillColor": "rgba(31, 118, 189, 0.18)", - "full": false, - "lineColor": "rgb(31, 120, 193)", - "show": false - }, - "tableColumn": "", - "targets": [ - { - "dsType": "influxdb", - "expr": "min(time() - process_start_time_seconds{instance=~\"$instance\"})", - "format": "time_series", - "groupBy": [ - { - "params": [ - "$__interval" - ], - "type": "time" - }, - { - "params": [ - "null" - ], - "type": "fill" - } - ], - "intervalFactor": 2, - "policy": "default", - "refId": "A", - "resultFormat": "time_series", - "select": [ - [ - { - "params": [ - "value" - ], - "type": "field" - }, - { - "params": [], - "type": "mean" - } - ] - ], - "step": 20, - "tags": [] - } - ], - "thresholds": "", - "timeFrom": null, - "title": "启动时间", - "type": "singlestat", - "valueFontSize": "80%", - "valueMaps": [ - { - "op": "=", - "text": "N/A", - "value": "null" - } - ], - "valueName": "current" - }, - { - "cacheTimeout": null, - "colorBackground": false, - "colorValue": true, - "colors": [ - "rgba(50, 172, 45, 0.97)", - "rgba(237, 129, 40, 0.89)", - "rgba(245, 54, 54, 0.9)" - ], - "datasource": "$datasource", - "format": "none", - "gauge": { - "maxValue": 100, - "minValue": 0, - "show": false, - "thresholdLabels": false, - "thresholdMarkers": true - }, - "gridPos": { - "h": 3, - "w": 4, - "x": 4, - "y": 0 - }, - "id": 79, - "interval": null, - "links": [], - "mappingType": 1, - "mappingTypes": [ - { - "name": "value to text", - "value": 1 - }, - { - "name": "range to text", - "value": 2 - } - ], - "maxDataPoints": 100, - "nullPointMode": "connected", - "nullText": null, - "postfix": "", - "postfixFontSize": "50%", - "prefix": "", - "prefixFontSize": "50%", - "rangeMaps": [ - { - "from": "null", - "text": "N/A", - "to": "null" - } - ], - "sparkline": { - "fillColor": "rgba(31, 118, 189, 0.18)", - "full": false, - "lineColor": "rgb(31, 120, 193)", - "show": false - }, - "tableColumn": "", - "targets": [ - { - "dsType": "influxdb", - "expr": "max(process_open_fds{instance=~\"$instance\"})", - "format": "time_series", - "groupBy": [ - { - "params": [ - "$__interval" - ], - "type": "time" - }, - { - "params": [ - "null" - ], - "type": "fill" - } - ], - "intervalFactor": 2, - "policy": "default", - "refId": "A", - "resultFormat": "time_series", - "select": [ - [ - { - "params": [ - "value" - ], - "type": "field" - }, - { - "params": [], - "type": "mean" - } - ] - ], - "step": 20, - "tags": [] - } - ], - "thresholds": "5000", - "title": "最大FDs", - "type": "singlestat", - "valueFontSize": "80%", - "valueMaps": [ - { - "op": "=", - "text": "N/A", - "value": "null" - } - ], - "valueName": "avg" - }, - { - "cacheTimeout": null, - "colorBackground": false, - "colorValue": true, - "colors": [ - "rgba(50, 172, 45, 0.97)", - "rgba(237, 129, 40, 0.89)", - "rgba(245, 54, 54, 0.9)" - ], - "datasource": "$datasource", - "decimals": 2, - "format": "decbytes", - "gauge": { - "maxValue": 100, - "minValue": 0, - "show": false, - "thresholdLabels": false, - "thresholdMarkers": true - }, - "gridPos": { - "h": 3, - "w": 4, - "x": 8, - "y": 0 - }, - "id": 81, - "interval": null, - "links": [], - "mappingType": 1, - "mappingTypes": [ - { - "name": "value to text", - "value": 1 - }, - { - "name": "range to text", - "value": 2 - } - ], - "maxDataPoints": 100, - "nullPointMode": "connected", - "nullText": null, - "postfix": "", - "postfixFontSize": "50%", - "prefix": "", - "prefixFontSize": "50%", - "rangeMaps": [ - { - "from": "null", - "text": "N/A", - "to": "null" - } - ], - "sparkline": { - "fillColor": "rgba(31, 118, 189, 0.18)", - "full": false, - "lineColor": "rgb(31, 120, 193)", - "show": false - }, - "tableColumn": "", - "targets": [ - { - "dsType": "influxdb", - "expr": "max(process_virtual_memory_bytes{instance=~\"$instance\"})", - "format": "time_series", - "groupBy": [ - { - "params": [ - "$__interval" - ], - "type": "time" - }, - { - "params": [ - "null" - ], - "type": "fill" - } - ], - "intervalFactor": 2, - "policy": "default", - "refId": "A", - "resultFormat": "time_series", - "select": [ - [ - { - "params": [ - "value" - ], - "type": "field" - }, - { - "params": [], - "type": "mean" - } - ] - ], - "step": 20, - "tags": [] - } - ], - "thresholds": "10240000000", - "title": "VM", - "type": "singlestat", - "valueFontSize": "80%", - "valueMaps": [ - { - "op": "=", - "text": "N/A", - "value": "null" - } - ], - "valueName": "avg" - }, - { - "cacheTimeout": null, - "colorBackground": false, - "colorValue": true, - "colors": [ - "rgba(50, 172, 45, 0.97)", - "rgba(237, 129, 40, 0.89)", - "rgba(245, 54, 54, 0.9)" - ], - "datasource": "$datasource", - "decimals": 2, - "format": "decbytes", - "gauge": { - "maxValue": 100, - "minValue": 0, - "show": false, - "thresholdLabels": false, - "thresholdMarkers": true - }, - "gridPos": { - "h": 3, - "w": 4, - "x": 12, - "y": 0 - }, - "id": 82, - "interval": null, - "links": [], - "mappingType": 1, - "mappingTypes": [ - { - "name": "value to text", - "value": 1 - }, - { - "name": "range to text", - "value": 2 - } - ], - "maxDataPoints": 100, - "nullPointMode": "connected", - "nullText": null, - "postfix": "", - "postfixFontSize": "50%", - "prefix": "", - "prefixFontSize": "50%", - "rangeMaps": [ - { - "from": "null", - "text": "N/A", - "to": "null" - } - ], - "sparkline": { - "fillColor": "rgba(31, 118, 189, 0.18)", - "full": false, - "lineColor": "rgb(31, 120, 193)", - "show": false - }, - "tableColumn": "", - "targets": [ - { - "dsType": "influxdb", - "expr": "max(process_resident_memory_bytes{instance=~\"$instance\"})", - "format": "time_series", - "groupBy": [ - { - "params": [ - "$__interval" - ], - "type": "time" - }, - { - "params": [ - "null" - ], - "type": "fill" - } - ], - "intervalFactor": 2, - "policy": "default", - "refId": "A", - "resultFormat": "time_series", - "select": [ - [ - { - "params": [ - "value" - ], - "type": "field" - }, - { - "params": [], - "type": "mean" - } - ] - ], - "step": 20, - "tags": [] - } - ], - "thresholds": "1024000000", - "title": "RM", - "type": "singlestat", - "valueFontSize": "80%", - "valueMaps": [ - { - "op": "=", - "text": "N/A", - "value": "null" - } - ], - "valueName": "avg" - }, - { - "cacheTimeout": null, - "colorBackground": false, - "colorValue": true, - "colors": [ - "#299c46", - "rgba(237, 129, 40, 0.89)", - "#d44a3a" - ], - "datasource": "$datasource", - "format": "percentunit", - "gauge": { - "maxValue": 100, - "minValue": 0, - "show": false, - "thresholdLabels": false, - "thresholdMarkers": true - }, - "gridPos": { - "h": 3, - "w": 4, - "x": 16, - "y": 0 - }, - "id": 16, - "interval": null, - "links": [], - "mappingType": 1, - "mappingTypes": [ - { - "name": "value to text", - "value": 1 - }, - { - "name": "range to text", - "value": 2 - } - ], - "maxDataPoints": 100, - "nullPointMode": "connected", - "nullText": null, - "postfix": "", - "postfixFontSize": "50%", - "prefix": "", - "prefixFontSize": "50%", - "rangeMaps": [ - { - "from": "null", - "text": "N/A", - "to": "null" - } - ], - "sparkline": { - "fillColor": "rgba(31, 118, 189, 0.18)", - "full": true, - "lineColor": "rgb(31, 120, 193)", - "show": true - }, - "tableColumn": "", - "targets": [ - { - "dsType": "influxdb", - "expr": "avg(irate(process_cpu_seconds_total{instance=~\"$instance\"}[5m]))", - "format": "time_series", - "groupBy": [ - { - "params": [ - "$__interval" - ], - "type": "time" - }, - { - "params": [ - "null" - ], - "type": "fill" - } - ], - "interval": "", - "intervalFactor": 1, - "legendFormat": "", - "policy": "default", - "refId": "A", - "resultFormat": "time_series", - "select": [ - [ - { - "params": [ - "value" - ], - "type": "field" - }, - { - "params": [], - "type": "mean" - } - ] - ], - "step": 60, - "tags": [] - } - ], - "thresholds": "1,10", - "timeFrom": null, - "timeShift": null, - "title": "系统 CPU", - "type": "singlestat", - "valueFontSize": "100%", - "valueMaps": [ - { - "op": "=", - "text": "N/A", - "value": "null" - } - ], - "valueName": "current" - }, - { - "cacheTimeout": null, - "colorBackground": false, - "colorValue": true, - "colors": [ - "#299c46", - "rgba(237, 129, 40, 0.89)", - "#d44a3a" - ], - "datasource": "$datasource", - "format": "short", - "gauge": { - "maxValue": 100, - "minValue": 0, - "show": false, - "thresholdLabels": false, - "thresholdMarkers": true - }, - "gridPos": { - "h": 3, - "w": 4, - "x": 20, - "y": 0 - }, - "id": 15, - "interval": null, - "links": [], - "mappingType": 1, - "mappingTypes": [ - { - "name": "value to text", - "value": 1 - }, - { - "name": "range to text", - "value": 2 - } - ], - "maxDataPoints": 100, - "nullPointMode": "connected", - "nullText": null, - "postfix": "", - "postfixFontSize": "50%", - "prefix": "", - "prefixFontSize": "50%", - "rangeMaps": [ - { - "from": "null", - "text": "N/A", - "to": "null" - } - ], - "sparkline": { - "fillColor": "rgba(31, 118, 189, 0.18)", - "full": false, - "lineColor": "rgb(31, 120, 193)", - "show": true - }, - "tableColumn": "", - "targets": [ - { - "dsType": "influxdb", - "expr": "avg(max(go_memstats_alloc_bytes{instance=~\"$instance\"}) by (instance))", - "format": "time_series", - "groupBy": [ - { - "params": [ - "$__interval" - ], - "type": "time" - }, - { - "params": [ - "null" - ], - "type": "fill" - } - ], - "interval": "", - "intervalFactor": 1, - "legendFormat": "", - "policy": "default", - "refId": "A", - "resultFormat": "time_series", - "select": [ - [ - { - "params": [ - "value" - ], - "type": "field" - }, - { - "params": [], - "type": "mean" - } - ] - ], - "step": 20, - "tags": [] - } - ], - "thresholds": "100000000", - "timeFrom": null, - "timeShift": null, - "title": "系统MEM", - "type": "singlestat", - "valueFontSize": "100%", - "valueMaps": [ - { - "op": "=", - "text": "N/A", - "value": "null" - } - ], - "valueName": "current" - }, - { - "aliasColors": {}, - "bars": false, - "dashLength": 10, - "dashes": false, - "datasource": "$datasource", - "decimals": null, - "description": "网络的每秒包数量", - "editable": true, - "error": false, - "fill": 6, - "grid": {}, - "gridPos": { - "h": 7, - "w": 12, - "x": 0, - "y": 3 - }, - "id": 264, - "legend": { - "alignAsTable": true, - "avg": true, - "current": false, - "hideEmpty": false, - "max": true, - "min": true, - "rightSide": false, - "show": true, - "sort": null, - "sortDesc": null, - "total": false, - "values": true - }, - "lines": true, - "linewidth": 2, - "links": [], - "nullPointMode": "null", - "percentage": false, - "pointradius": 5, - "points": false, - "renderer": "flot", - "seriesOverrides": [ - { - "alias": "Outbound pps", - "transform": "negative-Y" - } - ], - "spaceLength": 10, - "stack": false, - "steppedLine": false, - "targets": [ - { - "calculatedInterval": "2s", - "datasourceErrors": {}, - "errors": {}, - "expr": "irate(node_network_receive_packets_total{instance=\"$instance\", device!~\"^(lo|bond).*\"}[5m]) or irate(node_network_receive_packets{instance=\"$instance\", device!~\"^(lo|bond).*\"}[5m]) ", - "format": "time_series", - "hide": false, - "interval": "1m", - "intervalFactor": 1, - "legendFormat": "Inbound pps", - "metric": "", - "refId": "A", - "step": 120, - "target": "" - }, - { - "calculatedInterval": "2s", - "datasourceErrors": {}, - "errors": {}, - "expr": "irate(node_network_transmit_packets_total{instance_name=\"$instance\", device!~\"^(lo|bond).*\"}[5m]) or irate(node_network_transmit_packets{instance_name=\"$instance\", device!~\"^(lo|bond).*\"}[5m]) ", - "format": "time_series", - "hide": false, - "interval": "1m", - "intervalFactor": 1, - "legendFormat": "Outbound pps", - "metric": "", - "refId": "B", - "step": 120, - "target": "" - } - ], - "thresholds": [], - "timeFrom": null, - "timeRegions": [], - "timeShift": null, - "title": "Network Packet Per Sec", - "tooltip": { - "msResolution": false, - "shared": true, - "sort": 0, - "value_type": "individual" - }, - "type": "graph", - "xaxis": { - "buckets": null, - "mode": "time", - "name": null, - "show": true, - "values": [] - }, - "yaxes": [ - { - "format": "short", - "label": "", - "logBase": 1, - "max": null, - "min": null, - "show": true - }, - { - "format": "short", - "logBase": 1, - "max": null, - "min": null, - "show": true - } - ], - "yaxis": { - "align": false, - "alignLevel": null - } - }, - { - "aliasColors": {}, - "bars": false, - "dashLength": 10, - "dashes": false, - "datasource": "$datasource", - "decimals": null, - "description": "系统网络流量速率", - "editable": true, - "error": false, - "fill": 2, - "grid": {}, - "gridPos": { - "h": 7, - "w": 12, - "x": 12, - "y": 3 - }, - "id": 266, - "legend": { - "alignAsTable": true, - "avg": true, - "current": false, - "hideEmpty": false, - "max": true, - "min": true, - "rightSide": false, - "show": true, - "sort": null, - "sortDesc": null, - "total": false, - "values": true - }, - "lines": true, - "linewidth": 2, - "links": [], - "nullPointMode": "null", - "percentage": false, - "pointradius": 5, - "points": false, - "renderer": "flot", - "seriesOverrides": [ - { - "alias": "Outbound", - "transform": "negative-Y" - } - ], - "spaceLength": 10, - "stack": false, - "steppedLine": false, - "targets": [ - { - "calculatedInterval": "2s", - "datasourceErrors": {}, - "errors": {}, - "expr": "irate(node_network_receive_bytes{instance=\"$instance\", device!~\"^(lo|bond).*\"}[5m]) * 8 or irate(node_network_receive_bytes_total{instance=\"$instance\", device!~\"^(lo|bond).*\"}[5m]) * 8", - "format": "time_series", - "interval": "$interval", - "intervalFactor": 1, - "legendFormat": "Inbound", - "metric": "", - "refId": "B", - "step": 300, - "target": "" - }, - { - "calculatedInterval": "2s", - "datasourceErrors": {}, - "errors": {}, - "expr": "irate(node_network_transmit_bytes{instance=\"$instance\", device!~\"^(lo|bond).*\"}[5m]) * 8 or irate(node_network_transmit_bytes_total{instance=\"$instance\", device!~\"^(lo|bond).*\"}[5m]) * 8", - "format": "time_series", - "interval": "$interval", - "intervalFactor": 1, - "legendFormat": "Outbound", - "metric": "", - "refId": "A", - "step": 300, - "target": "" - } - ], - "thresholds": [], - "timeFrom": null, - "timeRegions": [], - "timeShift": null, - "title": "Network Traffic", - "tooltip": { - "msResolution": false, - "shared": true, - "sort": 0, - "value_type": "individual" - }, - "type": "graph", - "xaxis": { - "buckets": null, - "mode": "time", - "name": null, - "show": true, - "values": [] - }, - "yaxes": [ - { - "format": "bps", - "label": "", - "logBase": 1, - "max": null, - "min": null, - "show": true - }, - { - "format": "bps", - "logBase": 1, - "max": null, - "min": 0, - "show": true - } - ], - "yaxis": { - "align": false, - "alignLevel": null - } - }, - { - "collapsed": true, - "gridPos": { - "h": 1, - "w": 24, - "x": 0, - "y": 10 - }, - "id": 231, - "panels": [ - { - "aliasColors": {}, - "bars": false, - "dashLength": 10, - "dashes": false, - "datasource": "$datasource", - "fill": 1, - "gridPos": { - "h": 5, - "w": 8, - "x": 0, - "y": 4 - }, - "id": 208, - "legend": { - "alignAsTable": true, - "avg": false, - "current": true, - "max": true, - "min": true, - "show": true, - "sort": "current", - "sortDesc": true, - "total": false, - "values": true - }, - "lines": true, - "linewidth": 1, - "links": [], - "nullPointMode": "null", - "percentage": false, - "pointradius": 5, - "points": false, - "renderer": "flot", - "seriesOverrides": [], - "spaceLength": 10, - "stack": false, - "steppedLine": false, - "targets": [ - { - "dsType": "influxdb", - "expr": "avg(go_goroutines{instance=~\"$instance\"}) by (instance)", - "format": "time_series", - "groupBy": [ - { - "params": [ - "$__interval" - ], - "type": "time" - }, - { - "params": [ - "null" - ], - "type": "fill" - } - ], - "interval": "10s", - "intervalFactor": 2, - "legendFormat": "{{instance}}", - "policy": "default", - "refId": "A", - "resultFormat": "time_series", - "select": [ - [ - { - "params": [ - "value" - ], - "type": "field" - }, - { - "params": [], - "type": "mean" - } - ] - ], - "step": 20, - "tags": [] - } - ], - "thresholds": [], - "timeFrom": null, - "timeRegions": [], - "timeShift": null, - "title": "Rounine数量", - "tooltip": { - "shared": true, - "sort": 0, - "value_type": "individual" - }, - "type": "graph", - "xaxis": { - "buckets": null, - "mode": "time", - "name": null, - "show": true, - "values": [] - }, - "yaxes": [ - { - "format": "short", - "label": null, - "logBase": 1, - "max": null, - "min": null, - "show": true - }, - { - "format": "short", - "label": null, - "logBase": 1, - "max": null, - "min": null, - "show": true - } - ], - "yaxis": { - "align": false, - "alignLevel": null - } - }, - { - "aliasColors": {}, - "bars": false, - "dashLength": 10, - "dashes": false, - "datasource": "$datasource", - "fill": 1, - "gridPos": { - "h": 5, - "w": 8, - "x": 8, - "y": 4 - }, - "id": 210, - "legend": { - "alignAsTable": true, - "avg": false, - "current": true, - "max": true, - "min": true, - "show": true, - "sort": "current", - "sortDesc": true, - "total": false, - "values": true - }, - "lines": true, - "linewidth": 1, - "links": [], - "nullPointMode": "null", - "percentage": false, - "pointradius": 5, - "points": false, - "renderer": "flot", - "seriesOverrides": [], - "spaceLength": 10, - "stack": false, - "steppedLine": false, - "targets": [ - { - "dsType": "influxdb", - "expr": "avg(go_memstats_heap_objects{instance=~\"$instance\"}) by (instance)", - "format": "time_series", - "groupBy": [ - { - "params": [ - "$__interval" - ], - "type": "time" - }, - { - "params": [ - "null" - ], - "type": "fill" - } - ], - "hide": false, - "interval": "10s", - "intervalFactor": 2, - "legendFormat": "{{instance}}", - "policy": "default", - "refId": "A", - "resultFormat": "time_series", - "select": [ - [ - { - "params": [ - "value" - ], - "type": "field" - }, - { - "params": [], - "type": "mean" - } - ] - ], - "step": 20, - "tags": [] - } - ], - "thresholds": [], - "timeFrom": null, - "timeRegions": [], - "timeShift": null, - "title": "堆对象数", - "tooltip": { - "shared": true, - "sort": 0, - "value_type": "individual" - }, - "type": "graph", - "xaxis": { - "buckets": null, - "mode": "time", - "name": null, - "show": true, - "values": [] - }, - "yaxes": [ - { - "format": "short", - "label": null, - "logBase": 1, - "max": null, - "min": null, - "show": true - }, - { - "format": "short", - "label": null, - "logBase": 1, - "max": null, - "min": null, - "show": true - } - ], - "yaxis": { - "align": false, - "alignLevel": null - } - }, - { - "aliasColors": {}, - "bars": false, - "dashLength": 10, - "dashes": false, - "datasource": "$datasource", - "fill": 1, - "gridPos": { - "h": 5, - "w": 8, - "x": 16, - "y": 4 - }, - "id": 212, - "legend": { - "alignAsTable": true, - "avg": false, - "current": true, - "max": true, - "min": true, - "show": true, - "sort": "current", - "sortDesc": true, - "total": false, - "values": true - }, - "lines": true, - "linewidth": 1, - "links": [], - "nullPointMode": "null", - "percentage": false, - "pointradius": 5, - "points": false, - "renderer": "flot", - "seriesOverrides": [], - "spaceLength": 10, - "stack": false, - "steppedLine": false, - "targets": [ - { - "expr": "go_gc_duration_seconds{quantile=\"1\", instance=~\"$instance\" }", - "format": "time_series", - "intervalFactor": 1, - "legendFormat": "{{instance}} ", - "refId": "B" - } - ], - "thresholds": [], - "timeFrom": null, - "timeRegions": [], - "timeShift": null, - "title": "GC耗时", - "tooltip": { - "shared": true, - "sort": 0, - "value_type": "individual" - }, - "type": "graph", - "xaxis": { - "buckets": null, - "mode": "time", - "name": null, - "show": true, - "values": [] - }, - "yaxes": [ - { - "format": "s", - "label": null, - "logBase": 1, - "max": null, - "min": null, - "show": true - }, - { - "format": "short", - "label": null, - "logBase": 1, - "max": null, - "min": null, - "show": true - } - ], - "yaxis": { - "align": false, - "alignLevel": null - } - } - ], - "title": "Runtime", - "type": "row" - }, - { - "collapsed": true, - "gridPos": { - "h": 1, - "w": 24, - "x": 0, - "y": 11 - }, - "id": 200, - "panels": [ - { - "aliasColors": {}, - "bars": false, - "dashLength": 10, - "dashes": false, - "datasource": "$datasource", - "fill": 1, - "gridPos": { - "h": 8, - "w": 12, - "x": 0, - "y": 10 - }, - "id": 178, - "legend": { - "alignAsTable": true, - "avg": false, - "current": true, - "max": true, - "min": true, - "show": true, - "sort": "total", - "sortDesc": true, - "total": true, - "values": true - }, - "lines": true, - "linewidth": 1, - "links": [], - "nullPointMode": "null", - "percentage": false, - "pointradius": 5, - "points": false, - "renderer": "flot", - "seriesOverrides": [], - "spaceLength": 10, - "stack": false, - "steppedLine": false, - "targets": [ - { - "dsType": "influxdb", - "expr": "sum(rate(http_server_requests_duration_ms_count{instance=~\"$instance\",path!~\"register|.*ping\"}[2m])) by (path)", - "format": "time_series", - "groupBy": [ - { - "params": [ - "$__interval" - ], - "type": "time" - }, - { - "params": [ - "null" - ], - "type": "fill" - } - ], - "hide": false, - "interval": "10s", - "intervalFactor": 2, - "legendFormat": "{{path}}", - "policy": "default", - "refId": "A", - "resultFormat": "time_series", - "select": [ - [ - { - "params": [ - "value" - ], - "type": "field" - }, - { - "params": [], - "type": "mean" - } - ] - ], - "step": 20, - "tags": [] - } - ], - "thresholds": [], - "timeFrom": null, - "timeRegions": [], - "timeShift": null, - "title": "HTTP服务QPS", - "tooltip": { - "shared": true, - "sort": 0, - "value_type": "individual" - }, - "type": "graph", - "xaxis": { - "buckets": null, - "mode": "time", - "name": null, - "show": true, - "values": [] - }, - "yaxes": [ - { - "format": "short", - "label": null, - "logBase": 1, - "max": null, - "min": null, - "show": true - }, - { - "format": "short", - "label": null, - "logBase": 1, - "max": null, - "min": null, - "show": true - } - ], - "yaxis": { - "align": false, - "alignLevel": null - } - }, - { - "aliasColors": {}, - "bars": false, - "dashLength": 10, - "dashes": false, - "datasource": "$datasource", - "fill": 1, - "gridPos": { - "h": 8, - "w": 12, - "x": 12, - "y": 10 - }, - "id": 41, - "legend": { - "alignAsTable": true, - "avg": false, - "current": true, - "max": true, - "min": true, - "show": true, - "sort": "current", - "sortDesc": false, - "total": false, - "values": true - }, - "lines": true, - "linewidth": 1, - "links": [], - "nullPointMode": "null", - "percentage": false, - "pointradius": 5, - "points": false, - "renderer": "flot", - "seriesOverrides": [], - "spaceLength": 10, - "stack": false, - "steppedLine": false, - "targets": [ - { - "dsType": "influxdb", - "expr": "avg(increase(http_server_requests_duration_ms_sum{instance=~\"$instance\",path!~\"register|.*ping\"}[5m])/ increase(http_server_requests_duration_ms_count{instance=~\"$instance\",path!~\"register|.*ping\"}[5m]) >0) by (path)", - "format": "time_series", - "groupBy": [ - { - "params": [ - "$__interval" - ], - "type": "time" - }, - { - "params": [ - "null" - ], - "type": "fill" - } - ], - "intervalFactor": 2, - "legendFormat": "{{path}}", - "policy": "default", - "refId": "A", - "resultFormat": "time_series", - "select": [ - [ - { - "params": [ - "value" - ], - "type": "field" - }, - { - "params": [], - "type": "mean" - } - ] - ], - "step": 2, - "tags": [] - } - ], - "thresholds": [], - "timeFrom": null, - "timeRegions": [], - "timeShift": null, - "title": "HTTP服务平均耗时", - "tooltip": { - "shared": true, - "sort": 0, - "value_type": "individual" - }, - "type": "graph", - "xaxis": { - "buckets": null, - "mode": "time", - "name": null, - "show": true, - "values": [] - }, - "yaxes": [ - { - "format": "ms", - "label": null, - "logBase": 1, - "max": null, - "min": null, - "show": true - }, - { - "format": "short", - "label": null, - "logBase": 1, - "max": null, - "min": null, - "show": true - } - ], - "yaxis": { - "align": false, - "alignLevel": null - } - }, - { - "aliasColors": {}, - "breakPoint": "50%", - "cacheTimeout": null, - "combine": { - "label": "Others", - "threshold": 0 - }, - "datasource": "$datasource", - "fontSize": "80%", - "format": "short", - "gridPos": { - "h": 8, - "w": 24, - "x": 0, - "y": 18 - }, - "id": 248, - "interval": null, - "legend": { - "percentage": true, - "show": true, - "values": true - }, - "legendType": "Right side", - "links": [], - "maxDataPoints": 3, - "nullPointMode": "connected", - "pieType": "pie", - "strokeWidth": 1, - "targets": [ - { - "expr": "sum(irate(http_server_requests_duration_ms_count{instance=~\"$instance\",caller=~\"$http_user\",path=~\"$http_method\",path!~\"register|.*ping\"}[5m])) by (caller,path)", - "format": "time_series", - "groupBy": [ - { - "params": [ - "$__interval" - ], - "type": "time" - }, - { - "params": [ - "null" - ], - "type": "fill" - } - ], - "intervalFactor": 1, - "legendFormat": "{{caller}} => {{path}}", - "orderByTime": "ASC", - "policy": "default", - "refId": "A", - "resultFormat": "time_series", - "select": [ - [ - { - "params": [ - "value" - ], - "type": "field" - }, - { - "params": [], - "type": "mean" - } - ] - ], - "tags": [] - } - ], - "title": "HTTP caller 占比", - "type": "grafana-piechart-panel", - "valueName": "current" - }, - { - "aliasColors": {}, - "bars": true, - "dashLength": 10, - "dashes": false, - "datasource": "$datasource", - "fill": 1, - "gridPos": { - "h": 9, - "w": 12, - "x": 0, - "y": 26 - }, - "id": 250, - "legend": { - "alignAsTable": true, - "avg": false, - "current": true, - "max": true, - "min": true, - "show": true, - "sort": "max", - "sortDesc": true, - "total": false, - "values": true - }, - "lines": false, - "linewidth": 1, - "links": [], - "nullPointMode": "null", - "percentage": false, - "pointradius": 5, - "points": false, - "renderer": "flot", - "seriesOverrides": [], - "spaceLength": 10, - "stack": false, - "steppedLine": false, - "targets": [ - { - "dsType": "influxdb", - "expr": "sum(irate(http_server_requests_code_total{app=\"$app\",code!~\"0\",path=~\"$http_method\",caller=~\"$http_user\",env=\"$env\"}[5m])) BY (app, job, path, code) / IGNORING(code) GROUP_LEFT() sum(irate(http_server_requests_code_total{app=\"$app\",path=~\"$http_method\",caller=~\"$http_user\",env=\"$env\"}[5m])) BY (app, job, path) * 100", - "format": "time_series", - "groupBy": [ - { - "params": [ - "$__interval" - ], - "type": "time" - }, - { - "params": [ - "null" - ], - "type": "fill" - } - ], - "interval": "30s", - "intervalFactor": 2, - "legendFormat": "{{path}} : {{code}}", - "policy": "default", - "refId": "A", - "resultFormat": "time_series", - "select": [ - [ - { - "params": [ - "value" - ], - "type": "field" - }, - { - "params": [], - "type": "mean" - } - ] - ], - "step": 60, - "tags": [] - }, - { - "expr": "sum(irate(http_server_requests_code_total{instance=~\"$instance\",code!~\"0\",path=~\"$http_method\",caller=~\"$http_user\",path!~\"register|.*ping\"}[5m])) BY (path, code)", - "format": "time_series", - "hide": true, - "intervalFactor": 1, - "refId": "B" - }, - { - "expr": "sum(irate(http_server_requests_code_total{instance=~\"$instance\",path=~\"$http_method\",caller=~\"$http_user\",path!~\"register|.*ping\"}[5m])) BY ( path)", - "format": "time_series", - "hide": true, - "intervalFactor": 1, - "refId": "C" - } - ], - "thresholds": [], - "timeFrom": null, - "timeRegions": [], - "timeShift": null, - "title": " HTTP服务错误率", - "tooltip": { - "shared": true, - "sort": 0, - "value_type": "individual" - }, - "type": "graph", - "xaxis": { - "buckets": null, - "mode": "time", - "name": null, - "show": true, - "values": [] - }, - "yaxes": [ - { - "format": "percent", - "label": null, - "logBase": 1, - "max": null, - "min": null, - "show": true - }, - { - "format": "short", - "label": null, - "logBase": 1, - "max": null, - "min": null, - "show": true - } - ], - "yaxis": { - "align": false, - "alignLevel": null - } - }, - { - "aliasColors": {}, - "bars": false, - "dashLength": 10, - "dashes": false, - "datasource": "$datasource", - "fill": 1, - "gridPos": { - "h": 9, - "w": 12, - "x": 12, - "y": 26 - }, - "id": 42, - "legend": { - "alignAsTable": true, - "avg": true, - "current": true, - "max": true, - "min": true, - "show": true, - "sort": "current", - "sortDesc": false, - "total": false, - "values": true - }, - "lines": true, - "linewidth": 1, - "links": [], - "nullPointMode": "null", - "percentage": false, - "pointradius": 5, - "points": false, - "renderer": "flot", - "seriesOverrides": [], - "spaceLength": 10, - "stack": false, - "steppedLine": false, - "targets": [ - { - "dsType": "influxdb", - "expr": "sum(rate(http_server_requests_code_total{instance=~\"$instance\", code!=\"0\",path!~\"register|.*ping\"}[5m])) by (path, code)", - "format": "time_series", - "groupBy": [ - { - "params": [ - "$__interval" - ], - "type": "time" - }, - { - "params": [ - "null" - ], - "type": "fill" - } - ], - "instant": false, - "interval": "60s", - "intervalFactor": 1, - "legendFormat": "{{path}} : {{code}}", - "policy": "default", - "refId": "A", - "resultFormat": "time_series", - "select": [ - [ - { - "params": [ - "value" - ], - "type": "field" - }, - { - "params": [], - "type": "mean" - } - ] - ], - "step": 20, - "tags": [] - } - ], - "thresholds": [], - "timeFrom": null, - "timeRegions": [], - "timeShift": null, - "title": "HTTP服务错误返回码情况", - "tooltip": { - "shared": true, - "sort": 0, - "value_type": "individual" - }, - "type": "graph", - "xaxis": { - "buckets": null, - "mode": "time", - "name": null, - "show": true, - "values": [] - }, - "yaxes": [ - { - "format": "short", - "label": null, - "logBase": 1, - "max": null, - "min": null, - "show": true - }, - { - "format": "short", - "label": null, - "logBase": 1, - "max": null, - "min": null, - "show": true - } - ], - "yaxis": { - "align": false, - "alignLevel": null - } - }, - { - "aliasColors": {}, - "bars": false, - "dashLength": 10, - "dashes": false, - "datasource": "$datasource", - "fill": 1, - "gridPos": { - "h": 8, - "w": 12, - "x": 0, - "y": 35 - }, - "id": 252, - "legend": { - "alignAsTable": true, - "avg": false, - "current": true, - "max": true, - "min": true, - "show": true, - "sort": "current", - "sortDesc": false, - "total": false, - "values": true - }, - "lines": true, - "linewidth": 1, - "links": [], - "nullPointMode": "null", - "percentage": false, - "pointradius": 5, - "points": false, - "renderer": "flot", - "seriesOverrides": [], - "spaceLength": 10, - "stack": false, - "steppedLine": false, - "targets": [ - { - "expr": "histogram_quantile(0.99, sum(irate(http_server_requests_duration_ms_bucket{instance=~\"$instance\",path=~\"$http_method\",caller=~\"$http_user\",path!~\"register|.*ping\"}[5m])) by (path,le))", - "format": "time_series", - "hide": false, - "intervalFactor": 1, - "legendFormat": "pt99: {{path}}", - "refId": "B" - } - ], - "thresholds": [], - "timeFrom": null, - "timeRegions": [], - "timeShift": null, - "title": "[$app] HTTP服务 99分位响应时间", - "tooltip": { - "shared": true, - "sort": 0, - "value_type": "individual" - }, - "type": "graph", - "xaxis": { - "buckets": null, - "mode": "time", - "name": null, - "show": true, - "values": [] - }, - "yaxes": [ - { - "format": "ms", - "label": null, - "logBase": 1, - "max": null, - "min": null, - "show": true - }, - { - "format": "short", - "label": null, - "logBase": 1, - "max": null, - "min": null, - "show": true - } - ], - "yaxis": { - "align": false, - "alignLevel": null - } - }, - { - "aliasColors": {}, - "bars": false, - "dashLength": 10, - "dashes": false, - "datasource": "$datasource", - "fill": 1, - "gridPos": { - "h": 8, - "w": 12, - "x": 12, - "y": 35 - }, - "id": 254, - "legend": { - "alignAsTable": true, - "avg": false, - "current": true, - "max": true, - "min": true, - "show": true, - "sort": "current", - "sortDesc": false, - "total": false, - "values": true - }, - "lines": true, - "linewidth": 1, - "links": [], - "nullPointMode": "null", - "percentage": false, - "pointradius": 5, - "points": false, - "renderer": "flot", - "seriesOverrides": [], - "spaceLength": 10, - "stack": false, - "steppedLine": false, - "targets": [ - { - "expr": "histogram_quantile(0.90, sum(irate(http_server_requests_duration_ms_bucket{instance=~\"$instance\",path=~\"$http_method\",caller=~\"$http_user\",path!~\"register|.*ping\"}[5m])) by (path,le))", - "format": "time_series", - "intervalFactor": 1, - "legendFormat": "pt90: {{path}}", - "refId": "B" - } - ], - "thresholds": [], - "timeFrom": null, - "timeRegions": [], - "timeShift": null, - "title": "[$app] HTTP服务 90分位响应时间", - "tooltip": { - "shared": true, - "sort": 0, - "value_type": "individual" - }, - "type": "graph", - "xaxis": { - "buckets": null, - "mode": "time", - "name": null, - "show": true, - "values": [] - }, - "yaxes": [ - { - "format": "ms", - "label": null, - "logBase": 1, - "max": null, - "min": null, - "show": true - }, - { - "format": "short", - "label": null, - "logBase": 1, - "max": null, - "min": null, - "show": true - } - ], - "yaxis": { - "align": false, - "alignLevel": null - } - } - ], - "title": "HTTP", - "type": "row" - }, - { - "collapsed": true, - "gridPos": { - "h": 1, - "w": 24, - "x": 0, - "y": 12 - }, - "id": 220, - "panels": [ - { - "aliasColors": {}, - "bars": false, - "dashLength": 10, - "dashes": false, - "datasource": "$datasource", - "fill": 1, - "gridPos": { - "h": 7, - "w": 12, - "x": 0, - "y": 11 - }, - "id": 45, - "legend": { - "alignAsTable": true, - "avg": false, - "current": true, - "max": true, - "min": true, - "show": true, - "sort": "current", - "sortDesc": true, - "total": false, - "values": true - }, - "lines": true, - "linewidth": 1, - "links": [], - "nullPointMode": "null", - "percentage": false, - "pointradius": 5, - "points": false, - "renderer": "flot", - "seriesOverrides": [], - "spaceLength": 10, - "stack": false, - "steppedLine": false, - "targets": [ - { - "dsType": "influxdb", - "expr": "sum(rate(grpc_server_requests_duration_ms_count{instance=~\"$instance\",method!~\".*Ping\"}[2m])) by (method)", - "format": "time_series", - "groupBy": [ - { - "params": [ - "$__interval" - ], - "type": "time" - }, - { - "params": [ - "null" - ], - "type": "fill" - } - ], - "intervalFactor": 2, - "legendFormat": "grpc {{method}}", - "policy": "default", - "refId": "B", - "resultFormat": "time_series", - "select": [ - [ - { - "params": [ - "value" - ], - "type": "field" - }, - { - "params": [], - "type": "mean" - } - ] - ], - "step": 2, - "tags": [] - } - ], - "thresholds": [], - "timeFrom": null, - "timeRegions": [], - "timeShift": null, - "title": "GRPC服务QPS", - "tooltip": { - "shared": true, - "sort": 0, - "value_type": "individual" - }, - "type": "graph", - "xaxis": { - "buckets": null, - "mode": "time", - "name": null, - "show": true, - "values": [] - }, - "yaxes": [ - { - "format": "short", - "label": null, - "logBase": 1, - "max": null, - "min": null, - "show": true - }, - { - "format": "short", - "label": null, - "logBase": 1, - "max": null, - "min": null, - "show": true - } - ], - "yaxis": { - "align": false, - "alignLevel": null - } - }, - { - "aliasColors": {}, - "bars": false, - "dashLength": 10, - "dashes": false, - "datasource": "$datasource", - "fill": 1, - "gridPos": { - "h": 7, - "w": 12, - "x": 12, - "y": 11 - }, - "id": 46, - "legend": { - "alignAsTable": true, - "avg": false, - "current": true, - "max": true, - "min": true, - "show": true, - "sort": "current", - "sortDesc": true, - "total": false, - "values": true - }, - "lines": true, - "linewidth": 1, - "links": [], - "nullPointMode": "null", - "percentage": false, - "pointradius": 5, - "points": false, - "renderer": "flot", - "seriesOverrides": [], - "spaceLength": 10, - "stack": false, - "steppedLine": false, - "targets": [ - { - "dsType": "influxdb", - "expr": "avg(increase(grpc_server_requests_duration_ms_sum{instance=~\"$instance\",method!~\".*Ping\"}[5m])/ increase(grpc_server_requests_duration_ms_count{instance=~\"$instance\"}[5m])>0 ) by (method)", - "format": "time_series", - "groupBy": [ - { - "params": [ - "$__interval" - ], - "type": "time" - }, - { - "params": [ - "null" - ], - "type": "fill" - } - ], - "intervalFactor": 2, - "legendFormat": "{{method}}", - "policy": "default", - "refId": "B", - "resultFormat": "time_series", - "select": [ - [ - { - "params": [ - "value" - ], - "type": "field" - }, - { - "params": [], - "type": "mean" - } - ] - ], - "step": 2, - "tags": [] - } - ], - "thresholds": [], - "timeFrom": null, - "timeRegions": [], - "timeShift": null, - "title": "GRPC服务平均耗时", - "tooltip": { - "shared": true, - "sort": 0, - "value_type": "individual" - }, - "type": "graph", - "xaxis": { - "buckets": null, - "mode": "time", - "name": null, - "show": true, - "values": [] - }, - "yaxes": [ - { - "format": "ms", - "label": null, - "logBase": 1, - "max": null, - "min": null, - "show": true - }, - { - "format": "short", - "label": null, - "logBase": 1, - "max": null, - "min": null, - "show": true - } - ], - "yaxis": { - "align": false, - "alignLevel": null - } - }, - { - "aliasColors": {}, - "breakPoint": "50%", - "cacheTimeout": null, - "combine": { - "label": "Others", - "threshold": 0 - }, - "datasource": "$datasource", - "fontSize": "80%", - "format": "short", - "gridPos": { - "h": 8, - "w": 24, - "x": 0, - "y": 18 - }, - "id": 256, - "interval": null, - "legend": { - "percentage": true, - "show": true, - "values": true - }, - "legendType": "Right side", - "links": [], - "maxDataPoints": 3, - "nullPointMode": "connected", - "pieType": "pie", - "strokeWidth": 1, - "targets": [ - { - "expr": "sum(irate(grpc_server_requests_duration_ms_count{instance=~\"$instance\",caller=~\"$grpc_caller\",method!~\".*Ping\"}[5m])) by (caller,method)", - "format": "time_series", - "groupBy": [ - { - "params": [ - "$__interval" - ], - "type": "time" - }, - { - "params": [ - "null" - ], - "type": "fill" - } - ], - "intervalFactor": 1, - "legendFormat": "grpc {{caller}} => {{method}}", - "orderByTime": "ASC", - "policy": "default", - "refId": "B", - "resultFormat": "time_series", - "select": [ - [ - { - "params": [ - "value" - ], - "type": "field" - }, - { - "params": [], - "type": "mean" - } - ] - ], - "tags": [] - } - ], - "title": "RPC caller 占比", - "type": "grafana-piechart-panel", - "valueName": "current" - }, - { - "aliasColors": {}, - "bars": false, - "dashLength": 10, - "dashes": false, - "datasource": "$datasource", - "fill": 1, - "gridPos": { - "h": 9, - "w": 12, - "x": 0, - "y": 26 - }, - "id": 47, - "legend": { - "alignAsTable": true, - "avg": false, - "current": true, - "max": true, - "min": true, - "show": true, - "sort": "current", - "sortDesc": true, - "total": false, - "values": true - }, - "lines": true, - "linewidth": 1, - "links": [], - "nullPointMode": "null", - "percentage": false, - "pointradius": 5, - "points": false, - "renderer": "flot", - "seriesOverrides": [], - "spaceLength": 10, - "stack": false, - "steppedLine": false, - "targets": [ - { - "expr": "sum(rate(grpc_server_requests_code_total{instance=~\"$instance\", code!=\"0\",method!~\".*Ping\"}[5m])) by (method, code)", - "format": "time_series", - "intervalFactor": 1, - "legendFormat": "{{method}} {{code}}", - "refId": "A" - } - ], - "thresholds": [], - "timeFrom": null, - "timeRegions": [], - "timeShift": null, - "title": "GRPC错误码返回情况", - "tooltip": { - "shared": true, - "sort": 0, - "value_type": "individual" - }, - "type": "graph", - "xaxis": { - "buckets": null, - "mode": "time", - "name": null, - "show": true, - "values": [] - }, - "yaxes": [ - { - "format": "short", - "label": null, - "logBase": 1, - "max": null, - "min": null, - "show": true - }, - { - "format": "short", - "label": null, - "logBase": 1, - "max": null, - "min": null, - "show": true - } - ], - "yaxis": { - "align": false, - "alignLevel": null - } - }, - { - "aliasColors": {}, - "bars": true, - "dashLength": 10, - "dashes": false, - "datasource": "$datasource", - "fill": 1, - "gridPos": { - "h": 9, - "w": 12, - "x": 12, - "y": 26 - }, - "id": 258, - "legend": { - "alignAsTable": true, - "avg": false, - "current": true, - "max": true, - "min": true, - "show": true, - "sort": "current", - "sortDesc": true, - "total": false, - "values": true - }, - "lines": false, - "linewidth": 1, - "links": [], - "nullPointMode": "null", - "percentage": false, - "pointradius": 5, - "points": false, - "renderer": "flot", - "seriesOverrides": [], - "spaceLength": 10, - "stack": false, - "steppedLine": false, - "targets": [ - { - "dsType": "influxdb", - "expr": "sum(irate(grpc_server_requests_code_total{instance=~\"$instance\",code!~\"0\",caller=~\"$grpc_caller\",method!~\".*Ping\"}[5m])) BY ( method, code) / IGNORING(code) GROUP_LEFT() sum(irate(grpc_server_requests_code_total{instance=~\"$instance\",caller=~\"$grpc_caller\",method!~\".*Ping\"}[5m])) BY ( method) * 100", - "format": "time_series", - "groupBy": [ - { - "params": [ - "$__interval" - ], - "type": "time" - }, - { - "params": [ - "null" - ], - "type": "fill" - } - ], - "intervalFactor": 2, - "legendFormat": "grpc {{method}} : {{code}}", - "policy": "default", - "refId": "B", - "resultFormat": "time_series", - "select": [ - [ - { - "params": [ - "value" - ], - "type": "field" - }, - { - "params": [], - "type": "mean" - } - ] - ], - "step": 2, - "tags": [] - } - ], - "thresholds": [], - "timeFrom": null, - "timeRegions": [], - "timeShift": null, - "title": "RPC服务错误率", - "tooltip": { - "shared": true, - "sort": 0, - "value_type": "individual" - }, - "type": "graph", - "xaxis": { - "buckets": null, - "mode": "time", - "name": null, - "show": true, - "values": [] - }, - "yaxes": [ - { - "format": "percent", - "label": null, - "logBase": 1, - "max": null, - "min": null, - "show": true - }, - { - "format": "short", - "label": null, - "logBase": 1, - "max": null, - "min": null, - "show": true - } - ], - "yaxis": { - "align": false, - "alignLevel": null - } - }, - { - "aliasColors": {}, - "bars": false, - "dashLength": 10, - "dashes": false, - "datasource": "$datasource", - "fill": 1, - "gridPos": { - "h": 9, - "w": 12, - "x": 0, - "y": 35 - }, - "id": 260, - "legend": { - "alignAsTable": true, - "avg": false, - "current": true, - "max": true, - "min": true, - "show": true, - "sort": "max", - "sortDesc": true, - "total": false, - "values": true - }, - "lines": true, - "linewidth": 1, - "links": [], - "nullPointMode": "null", - "percentage": false, - "pointradius": 5, - "points": false, - "renderer": "flot", - "seriesOverrides": [], - "spaceLength": 10, - "stack": false, - "steppedLine": false, - "targets": [ - { - "expr": "histogram_quantile(0.99, sum(irate(grpc_server_requests_duration_ms_bucket{instance=~\"$instance\",method!~\".*Ping\",caller=~\"$grpc_caller\"}[5m])) by (le, method))", - "format": "time_series", - "intervalFactor": 1, - "legendFormat": "grpc: pt99: {{method}}", - "refId": "B" - } - ], - "thresholds": [], - "timeFrom": null, - "timeRegions": [], - "timeShift": null, - "title": "[$app] RPC 99分位响应时间", - "tooltip": { - "shared": true, - "sort": 0, - "value_type": "individual" - }, - "type": "graph", - "xaxis": { - "buckets": null, - "mode": "time", - "name": null, - "show": true, - "values": [] - }, - "yaxes": [ - { - "format": "ms", - "label": null, - "logBase": 1, - "max": null, - "min": null, - "show": true - }, - { - "format": "short", - "label": null, - "logBase": 1, - "max": null, - "min": null, - "show": true - } - ], - "yaxis": { - "align": false, - "alignLevel": null - } - }, - { - "aliasColors": {}, - "bars": false, - "dashLength": 10, - "dashes": false, - "datasource": "$datasource", - "fill": 1, - "gridPos": { - "h": 9, - "w": 12, - "x": 12, - "y": 35 - }, - "id": 262, - "legend": { - "alignAsTable": true, - "avg": false, - "current": true, - "max": true, - "min": true, - "show": true, - "sort": "current", - "sortDesc": true, - "total": false, - "values": true - }, - "lines": true, - "linewidth": 1, - "links": [], - "nullPointMode": "null", - "percentage": false, - "pointradius": 5, - "points": false, - "renderer": "flot", - "seriesOverrides": [], - "spaceLength": 10, - "stack": false, - "steppedLine": false, - "targets": [ - { - "expr": "histogram_quantile(0.90, sum(irate(grpc_server_requests_duration_ms_bucket{instance=~\"$instance\",method!~\".*Ping\",caller=~\"$grpc_caller\"}[5m])) by (le, method))", - "format": "time_series", - "intervalFactor": 1, - "legendFormat": "grpc: pt90: {{method}}", - "refId": "B" - } - ], - "thresholds": [], - "timeFrom": null, - "timeRegions": [], - "timeShift": null, - "title": "[$app] RPC 90分位响应时间", - "tooltip": { - "shared": true, - "sort": 0, - "value_type": "individual" - }, - "type": "graph", - "xaxis": { - "buckets": null, - "mode": "time", - "name": null, - "show": true, - "values": [] - }, - "yaxes": [ - { - "format": "ms", - "label": null, - "logBase": 1, - "max": null, - "min": null, - "show": true - }, - { - "format": "short", - "label": null, - "logBase": 1, - "max": null, - "min": null, - "show": true - } - ], - "yaxis": { - "align": false, - "alignLevel": null - } - } - ], - "title": "gRPC", - "type": "row" - }, - { - "collapsed": true, - "gridPos": { - "h": 1, - "w": 24, - "x": 0, - "y": 13 - }, - "id": 114, - "panels": [ - { - "aliasColors": {}, - "bars": false, - "dashLength": 10, - "dashes": false, - "datasource": "$datasource", - "fill": 1, - "gridPos": { - "h": 7, - "w": 8, - "x": 0, - "y": 12 - }, - "id": 244, - "legend": { - "alignAsTable": true, - "avg": false, - "current": true, - "max": true, - "min": true, - "show": true, - "sort": "current", - "sortDesc": true, - "total": false, - "values": true - }, - "lines": true, - "linewidth": 1, - "links": [], - "nullPointMode": "null", - "percentage": false, - "pointradius": 5, - "points": false, - "renderer": "flot", - "seriesOverrides": [], - "spaceLength": 10, - "stack": false, - "steppedLine": false, - "targets": [ - { - "dsType": "influxdb", - "expr": "sum(rate(grpc_client_requests_duration_ms_count{instance=~\"$instance\",method!~\".*Ping\"}[5m])) by (method)", - "format": "time_series", - "groupBy": [ - { - "params": [ - "$__interval" - ], - "type": "time" - }, - { - "params": [ - "null" - ], - "type": "fill" - } - ], - "intervalFactor": 1, - "legendFormat": "{{method}}", - "policy": "default", - "refId": "A", - "resultFormat": "time_series", - "select": [ - [ - { - "params": [ - "value" - ], - "type": "field" - }, - { - "params": [], - "type": "mean" - } - ] - ], - "step": 2, - "tags": [] - } - ], - "thresholds": [], - "timeFrom": null, - "timeRegions": [], - "timeShift": null, - "title": "gRPC依赖组件QPS", - "tooltip": { - "shared": true, - "sort": 0, - "value_type": "individual" - }, - "transparent": true, - "type": "graph", - "xaxis": { - "buckets": null, - "mode": "time", - "name": null, - "show": true, - "values": [] - }, - "yaxes": [ - { - "format": "short", - "label": null, - "logBase": 1, - "max": null, - "min": null, - "show": true - }, - { - "format": "short", - "label": null, - "logBase": 1, - "max": null, - "min": null, - "show": true - } - ], - "yaxis": { - "align": false, - "alignLevel": null - } - }, - { - "aliasColors": {}, - "bars": false, - "dashLength": 10, - "dashes": false, - "datasource": "$datasource", - "fill": 1, - "gridPos": { - "h": 7, - "w": 8, - "x": 8, - "y": 12 - }, - "id": 245, - "legend": { - "alignAsTable": true, - "avg": false, - "current": true, - "max": true, - "min": true, - "show": true, - "sort": null, - "sortDesc": null, - "total": false, - "values": true - }, - "lines": true, - "linewidth": 1, - "links": [], - "nullPointMode": "null", - "percentage": false, - "pointradius": 5, - "points": false, - "renderer": "flot", - "seriesOverrides": [], - "spaceLength": 10, - "stack": false, - "steppedLine": false, - "targets": [ - { - "dsType": "influxdb", - "expr": "avg(increase(grpc_client_requests_duration_ms_sum{instance=~\"$instance\",method!~\".*Ping\"}[5m])/ increase(grpc_client_requests_duration_ms_count{instance=~\"$instance\",method!~\".*Ping\"}[5m]) > 0 ) by (method)", - "format": "time_series", - "groupBy": [ - { - "params": [ - "$__interval" - ], - "type": "time" - }, - { - "params": [ - "null" - ], - "type": "fill" - } - ], - "hide": false, - "intervalFactor": 1, - "legendFormat": "{{method}}", - "policy": "default", - "refId": "A", - "resultFormat": "time_series", - "select": [ - [ - { - "params": [ - "value" - ], - "type": "field" - }, - { - "params": [], - "type": "mean" - } - ] - ], - "step": 240, - "tags": [] - } - ], - "thresholds": [], - "timeFrom": null, - "timeRegions": [], - "timeShift": null, - "title": "gRPC依赖组件平均响应时间", - "tooltip": { - "shared": true, - "sort": 0, - "value_type": "individual" - }, - "type": "graph", - "xaxis": { - "buckets": null, - "mode": "time", - "name": null, - "show": true, - "values": [] - }, - "yaxes": [ - { - "format": "ms", - "label": null, - "logBase": 1, - "max": null, - "min": null, - "show": true - }, - { - "format": "short", - "label": null, - "logBase": 1, - "max": null, - "min": null, - "show": true - } - ], - "yaxis": { - "align": false, - "alignLevel": null - } - }, - { - "aliasColors": {}, - "bars": false, - "dashLength": 10, - "dashes": false, - "datasource": "$datasource", - "fill": 1, - "gridPos": { - "h": 7, - "w": 8, - "x": 16, - "y": 12 - }, - "id": 232, - "legend": { - "alignAsTable": true, - "avg": false, - "current": true, - "max": true, - "min": true, - "show": true, - "sort": null, - "sortDesc": null, - "total": false, - "values": true - }, - "lines": true, - "linewidth": 1, - "links": [], - "nullPointMode": "null", - "percentage": false, - "pointradius": 5, - "points": false, - "renderer": "flot", - "seriesOverrides": [], - "spaceLength": 10, - "stack": false, - "steppedLine": false, - "targets": [ - { - "dsType": "influxdb", - "expr": "sum(irate(grpc_client_requests_code_total{instance=~\"$instance\",method!~\".*Ping\",code!=\"0\"}[5m])) by (method,code)", - "format": "time_series", - "groupBy": [ - { - "params": [ - "$__interval" - ], - "type": "time" - }, - { - "params": [ - "null" - ], - "type": "fill" - } - ], - "hide": false, - "intervalFactor": 1, - "legendFormat": "{{method}}: {{code}}", - "policy": "default", - "refId": "A", - "resultFormat": "time_series", - "select": [ - [ - { - "params": [ - "value" - ], - "type": "field" - }, - { - "params": [], - "type": "mean" - } - ] - ], - "step": 240, - "tags": [] - } - ], - "thresholds": [], - "timeFrom": null, - "timeRegions": [], - "timeShift": null, - "title": "gRPC依赖组件错误量", - "tooltip": { - "shared": true, - "sort": 0, - "value_type": "individual" - }, - "type": "graph", - "xaxis": { - "buckets": null, - "mode": "time", - "name": null, - "show": true, - "values": [] - }, - "yaxes": [ - { - "format": "none", - "label": null, - "logBase": 1, - "max": null, - "min": null, - "show": true - }, - { - "format": "short", - "label": null, - "logBase": 1, - "max": null, - "min": null, - "show": true - } - ], - "yaxis": { - "align": false, - "alignLevel": null - } - }, - { - "aliasColors": {}, - "bars": false, - "dashLength": 10, - "dashes": false, - "datasource": "$datasource", - "fill": 1, - "gridPos": { - "h": 7, - "w": 8, - "x": 0, - "y": 19 - }, - "id": 238, - "legend": { - "alignAsTable": true, - "avg": false, - "current": true, - "max": true, - "min": true, - "show": true, - "sort": "current", - "sortDesc": true, - "total": false, - "values": true - }, - "lines": true, - "linewidth": 1, - "links": [], - "nullPointMode": "null", - "percentage": false, - "pointradius": 5, - "points": false, - "renderer": "flot", - "seriesOverrides": [], - "spaceLength": 10, - "stack": false, - "steppedLine": false, - "targets": [ - { - "dsType": "influxdb", - "expr": "sum(rate(http_client_requests_duration_ms_count{instance=~\"$instance\",path!~\".*/discovery/.*\"}[5m])) by (path)", - "format": "time_series", - "groupBy": [ - { - "params": [ - "$__interval" - ], - "type": "time" - }, - { - "params": [ - "null" - ], - "type": "fill" - } - ], - "intervalFactor": 2, - "legendFormat": "{{path}}", - "policy": "default", - "refId": "A", - "resultFormat": "time_series", - "select": [ - [ - { - "params": [ - "value" - ], - "type": "field" - }, - { - "params": [], - "type": "mean" - } - ] - ], - "step": 2, - "tags": [] - } - ], - "thresholds": [], - "timeFrom": null, - "timeRegions": [], - "timeShift": null, - "title": "HTTP依赖组件QPS", - "tooltip": { - "shared": true, - "sort": 0, - "value_type": "individual" - }, - "type": "graph", - "xaxis": { - "buckets": null, - "mode": "time", - "name": null, - "show": true, - "values": [] - }, - "yaxes": [ - { - "format": "short", - "label": null, - "logBase": 1, - "max": null, - "min": null, - "show": true - }, - { - "format": "short", - "label": null, - "logBase": 1, - "max": null, - "min": null, - "show": true - } - ], - "yaxis": { - "align": false, - "alignLevel": null - } - }, - { - "aliasColors": {}, - "bars": false, - "dashLength": 10, - "dashes": false, - "datasource": "$datasource", - "fill": 1, - "gridPos": { - "h": 7, - "w": 8, - "x": 8, - "y": 19 - }, - "id": 239, - "legend": { - "alignAsTable": true, - "avg": false, - "current": true, - "max": true, - "min": true, - "show": true, - "sort": null, - "sortDesc": null, - "total": false, - "values": true - }, - "lines": true, - "linewidth": 1, - "links": [], - "nullPointMode": "null", - "percentage": false, - "pointradius": 5, - "points": false, - "renderer": "flot", - "seriesOverrides": [], - "spaceLength": 10, - "stack": false, - "steppedLine": false, - "targets": [ - { - "dsType": "influxdb", - "expr": "avg(increase(http_client_requests_duration_ms_sum{instance=~\"$instance\",path!~\".*/discovery/.*\"}[5m])/ increase(http_client_requests_duration_ms_count{instance=~\"$instance\",path!~\".*/discovery/.*\"}[5m]) > 0 ) by (path)", - "format": "time_series", - "groupBy": [ - { - "params": [ - "$__interval" - ], - "type": "time" - }, - { - "params": [ - "null" - ], - "type": "fill" - } - ], - "hide": false, - "intervalFactor": 2, - "legendFormat": "{{path}}", - "policy": "default", - "refId": "A", - "resultFormat": "time_series", - "select": [ - [ - { - "params": [ - "value" - ], - "type": "field" - }, - { - "params": [], - "type": "mean" - } - ] - ], - "step": 240, - "tags": [] - } - ], - "thresholds": [], - "timeFrom": null, - "timeRegions": [], - "timeShift": null, - "title": "HTTP依赖组件平均响应时间", - "tooltip": { - "shared": true, - "sort": 0, - "value_type": "individual" - }, - "type": "graph", - "xaxis": { - "buckets": null, - "mode": "time", - "name": null, - "show": true, - "values": [] - }, - "yaxes": [ - { - "format": "ms", - "label": null, - "logBase": 1, - "max": null, - "min": null, - "show": true - }, - { - "format": "short", - "label": null, - "logBase": 1, - "max": null, - "min": null, - "show": true - } - ], - "yaxis": { - "align": false, - "alignLevel": null - } - }, - { - "aliasColors": {}, - "bars": false, - "dashLength": 10, - "dashes": false, - "datasource": "$datasource", - "fill": 1, - "gridPos": { - "h": 7, - "w": 8, - "x": 16, - "y": 19 - }, - "id": 243, - "legend": { - "alignAsTable": true, - "avg": false, - "current": true, - "max": true, - "min": true, - "show": true, - "sort": null, - "sortDesc": null, - "total": false, - "values": true - }, - "lines": true, - "linewidth": 1, - "links": [], - "nullPointMode": "null", - "percentage": false, - "pointradius": 5, - "points": false, - "renderer": "flot", - "seriesOverrides": [], - "spaceLength": 10, - "stack": false, - "steppedLine": false, - "targets": [ - { - "dsType": "influxdb", - "expr": "sum(irate(http_client_requests_code_total{instance=~\"$instance\",path!~\".*/discovery/.*\",code!=\"0\"}[5m])) by (path,code)", - "format": "time_series", - "groupBy": [ - { - "params": [ - "$__interval" - ], - "type": "time" - }, - { - "params": [ - "null" - ], - "type": "fill" - } - ], - "hide": false, - "intervalFactor": 1, - "legendFormat": "{{path}}: {{code}}", - "policy": "default", - "refId": "A", - "resultFormat": "time_series", - "select": [ - [ - { - "params": [ - "value" - ], - "type": "field" - }, - { - "params": [], - "type": "mean" - } - ] - ], - "step": 240, - "tags": [] - } - ], - "thresholds": [], - "timeFrom": null, - "timeRegions": [], - "timeShift": null, - "title": "HTTP依赖组件错误量", - "tooltip": { - "shared": true, - "sort": 0, - "value_type": "individual" - }, - "type": "graph", - "xaxis": { - "buckets": null, - "mode": "time", - "name": null, - "show": true, - "values": [] - }, - "yaxes": [ - { - "format": "none", - "label": null, - "logBase": 1, - "max": null, - "min": null, - "show": true - }, - { - "format": "short", - "label": null, - "logBase": 1, - "max": null, - "min": null, - "show": true - } - ], - "yaxis": { - "align": false, - "alignLevel": null - } - }, - { - "aliasColors": {}, - "bars": false, - "dashLength": 10, - "dashes": false, - "datasource": "$datasource", - "fill": 1, - "gridPos": { - "h": 7, - "w": 8, - "x": 0, - "y": 26 - }, - "id": 59, - "legend": { - "alignAsTable": true, - "avg": false, - "current": true, - "max": true, - "min": true, - "show": true, - "sort": "current", - "sortDesc": true, - "total": false, - "values": true - }, - "lines": true, - "linewidth": 1, - "links": [], - "nullPointMode": "null", - "percentage": false, - "pointradius": 5, - "points": false, - "renderer": "flot", - "seriesOverrides": [], - "spaceLength": 10, - "stack": false, - "steppedLine": false, - "targets": [ - { - "dsType": "influxdb", - "expr": "sum(rate(mysql_client_requests_duration_ms_count{instance=~\"$instance\"}[5m])) by (command)", - "format": "time_series", - "groupBy": [ - { - "params": [ - "$__interval" - ], - "type": "time" - }, - { - "params": [ - "null" - ], - "type": "fill" - } - ], - "instant": false, - "interval": "", - "intervalFactor": 1, - "legendFormat": "{{command}}", - "policy": "default", - "refId": "A", - "resultFormat": "time_series", - "select": [ - [ - { - "params": [ - "value" - ], - "type": "field" - }, - { - "params": [], - "type": "mean" - } - ] - ], - "step": 2, - "tags": [] - } - ], - "thresholds": [], - "timeFrom": null, - "timeRegions": [], - "timeShift": null, - "title": "mysql组件QPS", - "tooltip": { - "shared": true, - "sort": 1, - "value_type": "individual" - }, - "transparent": true, - "type": "graph", - "xaxis": { - "buckets": null, - "mode": "time", - "name": null, - "show": true, - "values": [] - }, - "yaxes": [ - { - "format": "short", - "label": null, - "logBase": 1, - "max": null, - "min": null, - "show": true - }, - { - "format": "short", - "label": null, - "logBase": 1, - "max": null, - "min": null, - "show": true - } - ], - "yaxis": { - "align": false, - "alignLevel": null - } - }, - { - "aliasColors": {}, - "bars": false, - "dashLength": 10, - "dashes": false, - "datasource": "$datasource", - "fill": 1, - "gridPos": { - "h": 7, - "w": 8, - "x": 8, - "y": 26 - }, - "id": 62, - "legend": { - "alignAsTable": true, - "avg": false, - "current": true, - "max": true, - "min": true, - "show": true, - "sort": null, - "sortDesc": null, - "total": false, - "values": true - }, - "lines": true, - "linewidth": 1, - "links": [], - "nullPointMode": "null", - "percentage": false, - "pointradius": 5, - "points": false, - "renderer": "flot", - "seriesOverrides": [], - "spaceLength": 10, - "stack": false, - "steppedLine": false, - "targets": [ - { - "dsType": "influxdb", - "expr": "avg(increase(mysql_client_requests_duration_ms_sum{instance=~\"$instance\"}[5m])/ increase(mysql_client_requests_duration_ms_count{instance=~\"$instance\"}[5m]) > 0 ) by (command)", - "format": "time_series", - "groupBy": [ - { - "params": [ - "$__interval" - ], - "type": "time" - }, - { - "params": [ - "null" - ], - "type": "fill" - } - ], - "hide": false, - "intervalFactor": 2, - "legendFormat": "{{command}}", - "policy": "default", - "refId": "A", - "resultFormat": "time_series", - "select": [ - [ - { - "params": [ - "value" - ], - "type": "field" - }, - { - "params": [], - "type": "mean" - } - ] - ], - "step": 240, - "tags": [] - } - ], - "thresholds": [], - "timeFrom": null, - "timeRegions": [], - "timeShift": null, - "title": "mysql组件平均响应时间", - "tooltip": { - "shared": true, - "sort": 0, - "value_type": "individual" - }, - "type": "graph", - "xaxis": { - "buckets": null, - "mode": "time", - "name": null, - "show": true, - "values": [] - }, - "yaxes": [ - { - "format": "ms", - "label": null, - "logBase": 1, - "max": null, - "min": null, - "show": true - }, - { - "format": "short", - "label": null, - "logBase": 1, - "max": null, - "min": null, - "show": true - } - ], - "yaxis": { - "align": false, - "alignLevel": null - } - }, - { - "aliasColors": {}, - "bars": false, - "dashLength": 10, - "dashes": false, - "datasource": "$datasource", - "fill": 1, - "gridPos": { - "h": 7, - "w": 8, - "x": 16, - "y": 26 - }, - "id": 240, - "legend": { - "alignAsTable": true, - "avg": false, - "current": true, - "max": true, - "min": true, - "show": true, - "sort": null, - "sortDesc": null, - "total": false, - "values": true - }, - "lines": true, - "linewidth": 1, - "links": [], - "nullPointMode": "null", - "percentage": false, - "pointradius": 5, - "points": false, - "renderer": "flot", - "seriesOverrides": [], - "spaceLength": 10, - "stack": false, - "steppedLine": false, - "targets": [ - { - "dsType": "influxdb", - "expr": "sum(irate(mysql_client_requests_error_total{instance=~\"$instance\"}[5m])) by (error)", - "format": "time_series", - "groupBy": [ - { - "params": [ - "$__interval" - ], - "type": "time" - }, - { - "params": [ - "null" - ], - "type": "fill" - } - ], - "hide": false, - "intervalFactor": 1, - "legendFormat": "{{error}}", - "policy": "default", - "refId": "A", - "resultFormat": "time_series", - "select": [ - [ - { - "params": [ - "value" - ], - "type": "field" - }, - { - "params": [], - "type": "mean" - } - ] - ], - "step": 240, - "tags": [] - } - ], - "thresholds": [], - "timeFrom": null, - "timeRegions": [], - "timeShift": null, - "title": "mysql组件错误量", - "tooltip": { - "shared": true, - "sort": 0, - "value_type": "individual" - }, - "type": "graph", - "xaxis": { - "buckets": null, - "mode": "time", - "name": null, - "show": true, - "values": [] - }, - "yaxes": [ - { - "format": "none", - "label": null, - "logBase": 1, - "max": null, - "min": null, - "show": true - }, - { - "format": "short", - "label": null, - "logBase": 1, - "max": null, - "min": null, - "show": true - } - ], - "yaxis": { - "align": false, - "alignLevel": null - } - }, - { - "aliasColors": {}, - "bars": false, - "dashLength": 10, - "dashes": false, - "datasource": "$datasource", - "fill": 1, - "gridPos": { - "h": 7, - "w": 8, - "x": 0, - "y": 33 - }, - "id": 193, - "legend": { - "alignAsTable": true, - "avg": false, - "current": true, - "max": true, - "min": true, - "show": true, - "sort": "current", - "sortDesc": true, - "total": false, - "values": true - }, - "lines": true, - "linewidth": 1, - "links": [], - "nullPointMode": "null", - "percentage": false, - "pointradius": 5, - "points": false, - "renderer": "flot", - "seriesOverrides": [], - "spaceLength": 10, - "stack": false, - "steppedLine": false, - "targets": [ - { - "dsType": "influxdb", - "expr": "sum(rate(redis_client_requests_duration_ms_count{instance=~\"$instance\"}[5m])) by (command)", - "format": "time_series", - "groupBy": [ - { - "params": [ - "$__interval" - ], - "type": "time" - }, - { - "params": [ - "null" - ], - "type": "fill" - } - ], - "intervalFactor": 2, - "legendFormat": "redis {{command}}", - "policy": "default", - "refId": "A", - "resultFormat": "time_series", - "select": [ - [ - { - "params": [ - "value" - ], - "type": "field" - }, - { - "params": [], - "type": "mean" - } - ] - ], - "step": 2, - "tags": [] - } - ], - "thresholds": [], - "timeFrom": null, - "timeRegions": [], - "timeShift": null, - "title": "redis组件QPS", - "tooltip": { - "shared": true, - "sort": 0, - "value_type": "individual" - }, - "type": "graph", - "xaxis": { - "buckets": null, - "mode": "time", - "name": null, - "show": true, - "values": [] - }, - "yaxes": [ - { - "format": "short", - "label": null, - "logBase": 1, - "max": null, - "min": null, - "show": true - }, - { - "format": "short", - "label": null, - "logBase": 1, - "max": null, - "min": null, - "show": true - } - ], - "yaxis": { - "align": false, - "alignLevel": null - } - }, - { - "aliasColors": {}, - "bars": false, - "dashLength": 10, - "dashes": false, - "datasource": "$datasource", - "fill": 1, - "gridPos": { - "h": 7, - "w": 8, - "x": 8, - "y": 33 - }, - "id": 194, - "legend": { - "alignAsTable": true, - "avg": false, - "current": true, - "max": true, - "min": true, - "show": true, - "sort": null, - "sortDesc": null, - "total": false, - "values": true - }, - "lines": true, - "linewidth": 1, - "links": [], - "nullPointMode": "null", - "percentage": false, - "pointradius": 5, - "points": false, - "renderer": "flot", - "seriesOverrides": [], - "spaceLength": 10, - "stack": false, - "steppedLine": false, - "targets": [ - { - "dsType": "influxdb", - "expr": "avg(increase(redis_client_requests_duration_ms_sum{instance=~\"$instance\"}[5m])/ increase(redis_client_requests_duration_ms_count{instance=~\"$instance\"}[5m]) > 0 ) by (command)", - "format": "time_series", - "groupBy": [ - { - "params": [ - "$__interval" - ], - "type": "time" - }, - { - "params": [ - "null" - ], - "type": "fill" - } - ], - "hide": false, - "intervalFactor": 2, - "legendFormat": "{{command}}", - "policy": "default", - "refId": "B", - "resultFormat": "time_series", - "select": [ - [ - { - "params": [ - "value" - ], - "type": "field" - }, - { - "params": [], - "type": "mean" - } - ] - ], - "step": 240, - "tags": [] - } - ], - "thresholds": [], - "timeFrom": null, - "timeRegions": [], - "timeShift": null, - "title": "redis组件平均响应时间", - "tooltip": { - "shared": true, - "sort": 0, - "value_type": "individual" - }, - "type": "graph", - "xaxis": { - "buckets": null, - "mode": "time", - "name": null, - "show": true, - "values": [] - }, - "yaxes": [ - { - "format": "ms", - "label": null, - "logBase": 1, - "max": null, - "min": null, - "show": true - }, - { - "format": "short", - "label": null, - "logBase": 1, - "max": null, - "min": null, - "show": true - } - ], - "yaxis": { - "align": false, - "alignLevel": null - } - }, - { - "aliasColors": {}, - "bars": false, - "dashLength": 10, - "dashes": false, - "datasource": "$datasource", - "fill": 1, - "gridPos": { - "h": 7, - "w": 8, - "x": 16, - "y": 33 - }, - "id": 233, - "legend": { - "alignAsTable": true, - "avg": false, - "current": true, - "max": true, - "min": true, - "show": true, - "sort": null, - "sortDesc": null, - "total": false, - "values": true - }, - "lines": true, - "linewidth": 1, - "links": [], - "nullPointMode": "null", - "percentage": false, - "pointradius": 5, - "points": false, - "renderer": "flot", - "seriesOverrides": [], - "spaceLength": 10, - "stack": false, - "steppedLine": false, - "targets": [ - { - "dsType": "influxdb", - "expr": "sum(irate(redis_client_requests_error_total{instance=~\"$instance\"}[5m])) by (error)", - "format": "time_series", - "groupBy": [ - { - "params": [ - "$__interval" - ], - "type": "time" - }, - { - "params": [ - "null" - ], - "type": "fill" - } - ], - "hide": false, - "intervalFactor": 2, - "legendFormat": "{{error}}", - "policy": "default", - "refId": "B", - "resultFormat": "time_series", - "select": [ - [ - { - "params": [ - "value" - ], - "type": "field" - }, - { - "params": [], - "type": "mean" - } - ] - ], - "step": 240, - "tags": [] - } - ], - "thresholds": [], - "timeFrom": null, - "timeRegions": [], - "timeShift": null, - "title": "redis组件错误量", - "tooltip": { - "shared": true, - "sort": 0, - "value_type": "individual" - }, - "type": "graph", - "xaxis": { - "buckets": null, - "mode": "time", - "name": null, - "show": true, - "values": [] - }, - "yaxes": [ - { - "format": "none", - "label": null, - "logBase": 1, - "max": null, - "min": null, - "show": true - }, - { - "format": "short", - "label": null, - "logBase": 1, - "max": null, - "min": null, - "show": true - } - ], - "yaxis": { - "align": false, - "alignLevel": null - } - }, - { - "aliasColors": {}, - "bars": false, - "dashLength": 10, - "dashes": false, - "datasource": "$datasource", - "fill": 1, - "gridPos": { - "h": 7, - "w": 8, - "x": 0, - "y": 40 - }, - "id": 191, - "legend": { - "alignAsTable": true, - "avg": false, - "current": true, - "max": true, - "min": true, - "show": true, - "sort": "current", - "sortDesc": true, - "total": false, - "values": true - }, - "lines": true, - "linewidth": 1, - "links": [], - "nullPointMode": "null", - "percentage": false, - "pointradius": 5, - "points": false, - "renderer": "flot", - "seriesOverrides": [], - "spaceLength": 10, - "stack": false, - "steppedLine": false, - "targets": [ - { - "dsType": "influxdb", - "expr": "sum(rate(memcache_client_requests_duration_ms_count{instance=~\"$instance\"}[5m])) by (command)", - "format": "time_series", - "groupBy": [ - { - "params": [ - "$__interval" - ], - "type": "time" - }, - { - "params": [ - "null" - ], - "type": "fill" - } - ], - "intervalFactor": 2, - "legendFormat": "{{command}}", - "policy": "default", - "refId": "A", - "resultFormat": "time_series", - "select": [ - [ - { - "params": [ - "value" - ], - "type": "field" - }, - { - "params": [], - "type": "mean" - } - ] - ], - "step": 2, - "tags": [] - } - ], - "thresholds": [], - "timeFrom": null, - "timeRegions": [], - "timeShift": null, - "title": "memcache组件QPS", - "tooltip": { - "shared": true, - "sort": 0, - "value_type": "individual" - }, - "type": "graph", - "xaxis": { - "buckets": null, - "mode": "time", - "name": null, - "show": true, - "values": [] - }, - "yaxes": [ - { - "format": "short", - "label": null, - "logBase": 1, - "max": null, - "min": null, - "show": true - }, - { - "format": "short", - "label": null, - "logBase": 1, - "max": null, - "min": null, - "show": true - } - ], - "yaxis": { - "align": false, - "alignLevel": null - } - }, - { - "aliasColors": {}, - "bars": false, - "dashLength": 10, - "dashes": false, - "datasource": "$datasource", - "fill": 1, - "gridPos": { - "h": 7, - "w": 8, - "x": 8, - "y": 40 - }, - "id": 196, - "legend": { - "alignAsTable": true, - "avg": false, - "current": true, - "max": true, - "min": true, - "show": true, - "sort": null, - "sortDesc": null, - "total": false, - "values": true - }, - "lines": true, - "linewidth": 1, - "links": [], - "nullPointMode": "null", - "percentage": false, - "pointradius": 5, - "points": false, - "renderer": "flot", - "seriesOverrides": [], - "spaceLength": 10, - "stack": false, - "steppedLine": false, - "targets": [ - { - "dsType": "influxdb", - "expr": "avg(increase(memcache_client_requests_duration_ms_sum{instance=~\"$instance\"}[5m])/ increase(memcache_client_requests_duration_ms_count{instance=~\"$instance\"}[5m]) >0) by (command)", - "format": "time_series", - "groupBy": [ - { - "params": [ - "$__interval" - ], - "type": "time" - }, - { - "params": [ - "null" - ], - "type": "fill" - } - ], - "hide": false, - "intervalFactor": 2, - "legendFormat": "{{command}}", - "policy": "default", - "refId": "A", - "resultFormat": "time_series", - "select": [ - [ - { - "params": [ - "value" - ], - "type": "field" - }, - { - "params": [], - "type": "mean" - } - ] - ], - "step": 240, - "tags": [] - } - ], - "thresholds": [], - "timeFrom": null, - "timeRegions": [], - "timeShift": null, - "title": "memcache组件平均响应时间", - "tooltip": { - "shared": true, - "sort": 0, - "value_type": "individual" - }, - "type": "graph", - "xaxis": { - "buckets": null, - "mode": "time", - "name": null, - "show": true, - "values": [] - }, - "yaxes": [ - { - "format": "ms", - "label": null, - "logBase": 1, - "max": null, - "min": null, - "show": true - }, - { - "format": "short", - "label": null, - "logBase": 1, - "max": null, - "min": null, - "show": true - } - ], - "yaxis": { - "align": false, - "alignLevel": null - } - }, - { - "aliasColors": {}, - "bars": false, - "dashLength": 10, - "dashes": false, - "datasource": "$datasource", - "fill": 1, - "gridPos": { - "h": 7, - "w": 8, - "x": 16, - "y": 40 - }, - "id": 234, - "legend": { - "alignAsTable": true, - "avg": false, - "current": true, - "max": true, - "min": true, - "show": true, - "sort": null, - "sortDesc": null, - "total": false, - "values": true - }, - "lines": true, - "linewidth": 1, - "links": [], - "nullPointMode": "null", - "percentage": false, - "pointradius": 5, - "points": false, - "renderer": "flot", - "seriesOverrides": [], - "spaceLength": 10, - "stack": false, - "steppedLine": false, - "targets": [ - { - "dsType": "influxdb", - "expr": "sum(irate(memcache_client_requests_error_total{instance=~\"$instance\"}[5m])) by (error)", - "format": "time_series", - "groupBy": [ - { - "params": [ - "$__interval" - ], - "type": "time" - }, - { - "params": [ - "null" - ], - "type": "fill" - } - ], - "hide": false, - "intervalFactor": 2, - "legendFormat": "{{error}}", - "policy": "default", - "refId": "A", - "resultFormat": "time_series", - "select": [ - [ - { - "params": [ - "value" - ], - "type": "field" - }, - { - "params": [], - "type": "mean" - } - ] - ], - "step": 240, - "tags": [] - } - ], - "thresholds": [], - "timeFrom": null, - "timeRegions": [], - "timeShift": null, - "title": "memcache组件错误量", - "tooltip": { - "shared": true, - "sort": 0, - "value_type": "individual" - }, - "type": "graph", - "xaxis": { - "buckets": null, - "mode": "time", - "name": null, - "show": true, - "values": [] - }, - "yaxes": [ - { - "format": "none", - "label": null, - "logBase": 1, - "max": null, - "min": null, - "show": true - }, - { - "format": "short", - "label": null, - "logBase": 1, - "max": null, - "min": null, - "show": true - } - ], - "yaxis": { - "align": false, - "alignLevel": null - } - }, - { - "aliasColors": {}, - "bars": false, - "dashLength": 10, - "dashes": false, - "datasource": "$datasource", - "fill": 1, - "gridPos": { - "h": 7, - "w": 8, - "x": 0, - "y": 47 - }, - "id": 192, - "legend": { - "alignAsTable": true, - "avg": false, - "current": true, - "max": true, - "min": true, - "show": true, - "sort": "current", - "sortDesc": true, - "total": false, - "values": true - }, - "lines": true, - "linewidth": 1, - "links": [], - "nullPointMode": "null", - "percentage": false, - "pointradius": 5, - "points": false, - "renderer": "flot", - "seriesOverrides": [], - "spaceLength": 10, - "stack": false, - "steppedLine": false, - "targets": [ - { - "dsType": "influxdb", - "expr": "sum(rate(tidb_client_requests_duration_ms_count{instance=~\"$instance\"}[5m])) by (command)", - "format": "time_series", - "groupBy": [ - { - "params": [ - "$__interval" - ], - "type": "time" - }, - { - "params": [ - "null" - ], - "type": "fill" - } - ], - "intervalFactor": 2, - "legendFormat": "{{command}}", - "policy": "default", - "refId": "A", - "resultFormat": "time_series", - "select": [ - [ - { - "params": [ - "value" - ], - "type": "field" - }, - { - "params": [], - "type": "mean" - } - ] - ], - "step": 2, - "tags": [] - } - ], - "thresholds": [], - "timeFrom": null, - "timeRegions": [], - "timeShift": null, - "title": "tidb组件QPS", - "tooltip": { - "shared": true, - "sort": 0, - "value_type": "individual" - }, - "type": "graph", - "xaxis": { - "buckets": null, - "mode": "time", - "name": null, - "show": true, - "values": [] - }, - "yaxes": [ - { - "format": "short", - "label": null, - "logBase": 1, - "max": null, - "min": null, - "show": true - }, - { - "format": "short", - "label": null, - "logBase": 1, - "max": null, - "min": null, - "show": true - } - ], - "yaxis": { - "align": false, - "alignLevel": null - } - }, - { - "aliasColors": {}, - "bars": false, - "dashLength": 10, - "dashes": false, - "datasource": "$datasource", - "fill": 1, - "gridPos": { - "h": 7, - "w": 8, - "x": 8, - "y": 47 - }, - "id": 197, - "legend": { - "alignAsTable": true, - "avg": false, - "current": true, - "max": true, - "min": true, - "show": true, - "sort": null, - "sortDesc": null, - "total": false, - "values": true - }, - "lines": true, - "linewidth": 1, - "links": [], - "nullPointMode": "null", - "percentage": false, - "pointradius": 5, - "points": false, - "renderer": "flot", - "seriesOverrides": [], - "spaceLength": 10, - "stack": false, - "steppedLine": false, - "targets": [ - { - "dsType": "influxdb", - "expr": "avg(increase(tidb_client_requests_duration_ms_sum{instance=~\"$instance\"}[5m])/ increase(tidb_client_requests_duration_ms_count{instance=~\"$instance\"}[5m]) > 0 ) by (command)", - "format": "time_series", - "groupBy": [ - { - "params": [ - "$__interval" - ], - "type": "time" - }, - { - "params": [ - "null" - ], - "type": "fill" - } - ], - "hide": false, - "intervalFactor": 2, - "legendFormat": "{{command}}", - "policy": "default", - "refId": "A", - "resultFormat": "time_series", - "select": [ - [ - { - "params": [ - "value" - ], - "type": "field" - }, - { - "params": [], - "type": "mean" - } - ] - ], - "step": 240, - "tags": [] - } - ], - "thresholds": [], - "timeFrom": null, - "timeRegions": [], - "timeShift": null, - "title": "tidb组件平均响应时间", - "tooltip": { - "shared": true, - "sort": 0, - "value_type": "individual" - }, - "type": "graph", - "xaxis": { - "buckets": null, - "mode": "time", - "name": null, - "show": true, - "values": [] - }, - "yaxes": [ - { - "format": "ms", - "label": null, - "logBase": 1, - "max": null, - "min": null, - "show": true - }, - { - "format": "short", - "label": null, - "logBase": 1, - "max": null, - "min": null, - "show": true - } - ], - "yaxis": { - "align": false, - "alignLevel": null - } - }, - { - "aliasColors": {}, - "bars": false, - "dashLength": 10, - "dashes": false, - "datasource": "$datasource", - "fill": 1, - "gridPos": { - "h": 7, - "w": 8, - "x": 16, - "y": 47 - }, - "id": 235, - "legend": { - "alignAsTable": true, - "avg": false, - "current": true, - "max": true, - "min": true, - "show": true, - "sort": null, - "sortDesc": null, - "total": false, - "values": true - }, - "lines": true, - "linewidth": 1, - "links": [], - "nullPointMode": "null", - "percentage": false, - "pointradius": 5, - "points": false, - "renderer": "flot", - "seriesOverrides": [], - "spaceLength": 10, - "stack": false, - "steppedLine": false, - "targets": [ - { - "dsType": "influxdb", - "expr": "sum(irate(tidb_client_requests_error_total{instance=~\"$instance\"}[5m])) by (error)", - "format": "time_series", - "groupBy": [ - { - "params": [ - "$__interval" - ], - "type": "time" - }, - { - "params": [ - "null" - ], - "type": "fill" - } - ], - "hide": false, - "intervalFactor": 2, - "legendFormat": "{{error}}", - "policy": "default", - "refId": "A", - "resultFormat": "time_series", - "select": [ - [ - { - "params": [ - "value" - ], - "type": "field" - }, - { - "params": [], - "type": "mean" - } - ] - ], - "step": 240, - "tags": [] - } - ], - "thresholds": [], - "timeFrom": null, - "timeRegions": [], - "timeShift": null, - "title": "tidb组件错误量", - "tooltip": { - "shared": true, - "sort": 0, - "value_type": "individual" - }, - "type": "graph", - "xaxis": { - "buckets": null, - "mode": "time", - "name": null, - "show": true, - "values": [] - }, - "yaxes": [ - { - "format": "none", - "label": null, - "logBase": 1, - "max": null, - "min": null, - "show": true - }, - { - "format": "short", - "label": null, - "logBase": 1, - "max": null, - "min": null, - "show": true - } - ], - "yaxis": { - "align": false, - "alignLevel": null - } - }, - { - "aliasColors": {}, - "bars": false, - "dashLength": 10, - "dashes": false, - "datasource": "$datasource", - "fill": 1, - "gridPos": { - "h": 7, - "w": 8, - "x": 0, - "y": 54 - }, - "id": 190, - "legend": { - "alignAsTable": true, - "avg": false, - "current": true, - "max": true, - "min": true, - "show": true, - "sort": "current", - "sortDesc": true, - "total": false, - "values": true - }, - "lines": true, - "linewidth": 1, - "links": [], - "nullPointMode": "null", - "percentage": false, - "pointradius": 5, - "points": false, - "renderer": "flot", - "seriesOverrides": [], - "spaceLength": 10, - "stack": false, - "steppedLine": false, - "targets": [ - { - "dsType": "influxdb", - "expr": "sum(rate(hbase_client_requests_duration_ms_count{instance=~\"$instance\"}[5m])) by (command)", - "format": "time_series", - "groupBy": [ - { - "params": [ - "$__interval" - ], - "type": "time" - }, - { - "params": [ - "null" - ], - "type": "fill" - } - ], - "intervalFactor": 2, - "legendFormat": "{{command}}", - "policy": "default", - "refId": "A", - "resultFormat": "time_series", - "select": [ - [ - { - "params": [ - "value" - ], - "type": "field" - }, - { - "params": [], - "type": "mean" - } - ] - ], - "step": 2, - "tags": [] - } - ], - "thresholds": [], - "timeFrom": null, - "timeRegions": [], - "timeShift": null, - "title": "hbase组件QPS", - "tooltip": { - "shared": true, - "sort": 0, - "value_type": "individual" - }, - "type": "graph", - "xaxis": { - "buckets": null, - "mode": "time", - "name": null, - "show": true, - "values": [] - }, - "yaxes": [ - { - "format": "short", - "label": null, - "logBase": 1, - "max": null, - "min": null, - "show": true - }, - { - "format": "short", - "label": null, - "logBase": 1, - "max": null, - "min": null, - "show": true - } - ], - "yaxis": { - "align": false, - "alignLevel": null - } - }, - { - "aliasColors": {}, - "bars": false, - "dashLength": 10, - "dashes": false, - "datasource": "$datasource", - "fill": 1, - "gridPos": { - "h": 7, - "w": 8, - "x": 8, - "y": 54 - }, - "id": 198, - "legend": { - "alignAsTable": true, - "avg": false, - "current": true, - "max": true, - "min": true, - "show": true, - "sort": null, - "sortDesc": null, - "total": false, - "values": true - }, - "lines": true, - "linewidth": 1, - "links": [], - "nullPointMode": "null", - "percentage": false, - "pointradius": 5, - "points": false, - "renderer": "flot", - "seriesOverrides": [], - "spaceLength": 10, - "stack": false, - "steppedLine": false, - "targets": [ - { - "dsType": "influxdb", - "expr": "avg(increase(hbase_client_requests_duration_ms_sum{instance=~\"$instance\"}[5m])/ increase(hbase_client_requests_duration_ms_count{instance=~\"$instance\"}[5m]) > 0 ) by (command)", - "format": "time_series", - "groupBy": [ - { - "params": [ - "$__interval" - ], - "type": "time" - }, - { - "params": [ - "null" - ], - "type": "fill" - } - ], - "hide": false, - "intervalFactor": 2, - "legendFormat": "{{command}}", - "policy": "default", - "refId": "A", - "resultFormat": "time_series", - "select": [ - [ - { - "params": [ - "value" - ], - "type": "field" - }, - { - "params": [], - "type": "mean" - } - ] - ], - "step": 240, - "tags": [] - } - ], - "thresholds": [], - "timeFrom": null, - "timeRegions": [], - "timeShift": null, - "title": "hbase组件平均响应时间", - "tooltip": { - "shared": true, - "sort": 0, - "value_type": "individual" - }, - "type": "graph", - "xaxis": { - "buckets": null, - "mode": "time", - "name": null, - "show": true, - "values": [] - }, - "yaxes": [ - { - "format": "ms", - "label": null, - "logBase": 1, - "max": null, - "min": null, - "show": true - }, - { - "format": "short", - "label": null, - "logBase": 1, - "max": null, - "min": null, - "show": true - } - ], - "yaxis": { - "align": false, - "alignLevel": null - } - }, - { - "aliasColors": {}, - "bars": false, - "dashLength": 10, - "dashes": false, - "datasource": "$datasource", - "fill": 1, - "gridPos": { - "h": 7, - "w": 8, - "x": 16, - "y": 54 - }, - "id": 236, - "legend": { - "alignAsTable": true, - "avg": false, - "current": true, - "max": true, - "min": true, - "show": true, - "sort": null, - "sortDesc": null, - "total": false, - "values": true - }, - "lines": true, - "linewidth": 1, - "links": [], - "nullPointMode": "null", - "percentage": false, - "pointradius": 5, - "points": false, - "renderer": "flot", - "seriesOverrides": [], - "spaceLength": 10, - "stack": false, - "steppedLine": false, - "targets": [ - { - "dsType": "influxdb", - "expr": "sum(irate(hbase_client_requests_error_total{instance=~\"$instance\"}[5m])) by (error)", - "format": "time_series", - "groupBy": [ - { - "params": [ - "$__interval" - ], - "type": "time" - }, - { - "params": [ - "null" - ], - "type": "fill" - } - ], - "hide": false, - "intervalFactor": 2, - "legendFormat": "{{error}}", - "policy": "default", - "refId": "A", - "resultFormat": "time_series", - "select": [ - [ - { - "params": [ - "value" - ], - "type": "field" - }, - { - "params": [], - "type": "mean" - } - ] - ], - "step": 240, - "tags": [] - } - ], - "thresholds": [], - "timeFrom": null, - "timeRegions": [], - "timeShift": null, - "title": "hbase组件平错误量", - "tooltip": { - "shared": true, - "sort": 0, - "value_type": "individual" - }, - "type": "graph", - "xaxis": { - "buckets": null, - "mode": "time", - "name": null, - "show": true, - "values": [] - }, - "yaxes": [ - { - "format": "none", - "label": null, - "logBase": 1, - "max": null, - "min": null, - "show": true - }, - { - "format": "short", - "label": null, - "logBase": 1, - "max": null, - "min": null, - "show": true - } - ], - "yaxis": { - "align": false, - "alignLevel": null - } - }, - { - "aliasColors": {}, - "bars": false, - "dashLength": 10, - "dashes": false, - "datasource": "$datasource", - "fill": 1, - "gridPos": { - "h": 7, - "w": 8, - "x": 0, - "y": 61 - }, - "id": 189, - "legend": { - "alignAsTable": true, - "avg": false, - "current": true, - "max": true, - "min": true, - "show": true, - "sort": "current", - "sortDesc": true, - "total": false, - "values": true - }, - "lines": true, - "linewidth": 1, - "links": [], - "nullPointMode": "null", - "percentage": false, - "pointradius": 5, - "points": false, - "renderer": "flot", - "seriesOverrides": [], - "spaceLength": 10, - "stack": false, - "steppedLine": false, - "targets": [ - { - "dsType": "influxdb", - "expr": "sum(rate(databus_client_requests_duration_ms_count{instance=~\"$instance\"}[5m])) by (command)", - "format": "time_series", - "groupBy": [ - { - "params": [ - "$__interval" - ], - "type": "time" - }, - { - "params": [ - "null" - ], - "type": "fill" - } - ], - "intervalFactor": 2, - "legendFormat": "{{command}}", - "policy": "default", - "refId": "A", - "resultFormat": "time_series", - "select": [ - [ - { - "params": [ - "value" - ], - "type": "field" - }, - { - "params": [], - "type": "mean" - } - ] - ], - "step": 2, - "tags": [] - } - ], - "thresholds": [], - "timeFrom": null, - "timeRegions": [], - "timeShift": null, - "title": "databus组件QPS", - "tooltip": { - "shared": true, - "sort": 0, - "value_type": "individual" - }, - "type": "graph", - "xaxis": { - "buckets": null, - "mode": "time", - "name": null, - "show": true, - "values": [] - }, - "yaxes": [ - { - "format": "short", - "label": null, - "logBase": 1, - "max": null, - "min": null, - "show": true - }, - { - "format": "short", - "label": null, - "logBase": 1, - "max": null, - "min": null, - "show": true - } - ], - "yaxis": { - "align": false, - "alignLevel": null - } - }, - { - "aliasColors": {}, - "bars": false, - "dashLength": 10, - "dashes": false, - "datasource": "$datasource", - "fill": 1, - "gridPos": { - "h": 7, - "w": 8, - "x": 8, - "y": 61 - }, - "id": 195, - "legend": { - "alignAsTable": true, - "avg": false, - "current": true, - "max": true, - "min": true, - "show": true, - "sort": null, - "sortDesc": null, - "total": false, - "values": true - }, - "lines": true, - "linewidth": 1, - "links": [], - "nullPointMode": "null", - "percentage": false, - "pointradius": 5, - "points": false, - "renderer": "flot", - "seriesOverrides": [], - "spaceLength": 10, - "stack": false, - "steppedLine": false, - "targets": [ - { - "dsType": "influxdb", - "expr": "avg(increase(databus_client_requests_duration_ms_sum{instance=~\"$instance\"}[5m])/ increase(databus_client_requests_duration_ms_count{instance=~\"$instance\"}[5m]) > 0 ) by (command)", - "format": "time_series", - "groupBy": [ - { - "params": [ - "$__interval" - ], - "type": "time" - }, - { - "params": [ - "null" - ], - "type": "fill" - } - ], - "hide": false, - "intervalFactor": 2, - "legendFormat": "{{command}}", - "policy": "default", - "refId": "A", - "resultFormat": "time_series", - "select": [ - [ - { - "params": [ - "value" - ], - "type": "field" - }, - { - "params": [], - "type": "mean" - } - ] - ], - "step": 240, - "tags": [] - } - ], - "thresholds": [], - "timeFrom": null, - "timeRegions": [], - "timeShift": null, - "title": "databus组件平均响应时间", - "tooltip": { - "shared": true, - "sort": 0, - "value_type": "individual" - }, - "type": "graph", - "xaxis": { - "buckets": null, - "mode": "time", - "name": null, - "show": true, - "values": [] - }, - "yaxes": [ - { - "format": "ms", - "label": null, - "logBase": 1, - "max": null, - "min": null, - "show": true - }, - { - "format": "short", - "label": null, - "logBase": 1, - "max": null, - "min": null, - "show": true - } - ], - "yaxis": { - "align": false, - "alignLevel": null - } - }, - { - "aliasColors": {}, - "bars": false, - "dashLength": 10, - "dashes": false, - "datasource": "$datasource", - "fill": 1, - "gridPos": { - "h": 7, - "w": 8, - "x": 16, - "y": 61 - }, - "id": 237, - "legend": { - "alignAsTable": true, - "avg": false, - "current": true, - "max": true, - "min": true, - "show": true, - "sort": null, - "sortDesc": null, - "total": false, - "values": true - }, - "lines": true, - "linewidth": 1, - "links": [], - "nullPointMode": "null", - "percentage": false, - "pointradius": 5, - "points": false, - "renderer": "flot", - "seriesOverrides": [], - "spaceLength": 10, - "stack": false, - "steppedLine": false, - "targets": [ - { - "dsType": "influxdb", - "expr": "sum(irate(databus_client_requests_error_total{instance=~\"$instance\"}[5m])) by (error)", - "format": "time_series", - "groupBy": [ - { - "params": [ - "$__interval" - ], - "type": "time" - }, - { - "params": [ - "null" - ], - "type": "fill" - } - ], - "hide": false, - "intervalFactor": 2, - "legendFormat": "{{error}}", - "policy": "default", - "refId": "A", - "resultFormat": "time_series", - "select": [ - [ - { - "params": [ - "value" - ], - "type": "field" - }, - { - "params": [], - "type": "mean" - } - ] - ], - "step": 240, - "tags": [] - } - ], - "thresholds": [], - "timeFrom": null, - "timeRegions": [], - "timeShift": null, - "title": "databus组件错误量", - "tooltip": { - "shared": true, - "sort": 0, - "value_type": "individual" - }, - "type": "graph", - "xaxis": { - "buckets": null, - "mode": "time", - "name": null, - "show": true, - "values": [] - }, - "yaxes": [ - { - "format": "none", - "label": null, - "logBase": 1, - "max": null, - "min": null, - "show": true - }, - { - "format": "short", - "label": null, - "logBase": 1, - "max": null, - "min": null, - "show": true - } - ], - "yaxis": { - "align": false, - "alignLevel": null - } - } - ], - "repeat": null, - "title": "Component", - "type": "row" - } - ], - "refresh": false, - "schemaVersion": 18, - "style": "dark", - "tags": [ - "Go", - "业务监控" - ], - "templating": { - "list": [ - { - "hide": 2, - "includeAll": false, - "label": null, - "multi": true, - "name": "datasource", - "options": [], - "query": "prometheus", - "refresh": 1, - "skipUrlSync": false, - "type": "datasource" - }, - { - "allValue": ".*", - "current": { - "text": "All", - "value": [ - "$__all" - ] - }, - "datasource": "$datasource", - "definition": "label_values(go_goroutines,instance)", - "hide": 0, - "includeAll": true, - "label": "实例", - "multi": true, - "name": "instance", - "options": [], - "query": "label_values(go_goroutines,instance)", - "refresh": 1, - "regex": "", - "skipUrlSync": false, - "sort": 1, - "tagValuesQuery": "", - "tags": [], - "tagsQuery": "", - "type": "query", - "useTags": false - }, - { - "allValue": null, - "current": { - "text": "All", - "value": [ - "$__all" - ] - }, - "datasource": "$datasource", - "definition": "label_values(http_server_requests_duration_ms_count{path!=\"\",instance=~\"$instance\"}, path)", - "hide": 0, - "includeAll": true, - "label": "http接口", - "multi": true, - "name": "http_method", - "options": [], - "query": "label_values(http_server_requests_duration_ms_count{path!=\"\",instance=~\"$instance\"}, path)", - "refresh": 1, - "regex": "", - "skipUrlSync": false, - "sort": 1, - "tagValuesQuery": "", - "tags": [], - "tagsQuery": "", - "type": "query", - "useTags": false - }, - { - "allValue": null, - "current": { - "text": "All", - "value": [ - "$__all" - ] - }, - "datasource": "$datasource", - "definition": "label_values(http_server_requests_duration_ms_count{caller!=\"\",instance=~\"$instance\"}, caller)", - "hide": 0, - "includeAll": true, - "label": "http调用者", - "multi": true, - "name": "http_user", - "options": [], - "query": "label_values(http_server_requests_duration_ms_count{caller!=\"\",instance=~\"$instance\"}, caller)", - "refresh": 1, - "regex": "", - "skipUrlSync": false, - "sort": 0, - "tagValuesQuery": "", - "tags": [], - "tagsQuery": "", - "type": "query", - "useTags": false - }, - { - "allValue": null, - "current": { - "text": "All", - "value": [ - "$__all" - ] - }, - "datasource": "$datasource", - "definition": "label_values(http_client_requests_duration_ms_count{path!=\"\",instance=~\"$instance\"}, path)", - "hide": 0, - "includeAll": true, - "label": "http依赖接口", - "multi": true, - "name": "http_client_method", - "options": [], - "query": "label_values(http_client_requests_duration_ms_count{path!=\"\",instance=~\"$instance\"}, path)", - "refresh": 1, - "regex": "", - "skipUrlSync": false, - "sort": 1, - "tagValuesQuery": "", - "tags": [], - "tagsQuery": "", - "type": "query", - "useTags": false - }, - { - "allValue": null, - "current": { - "text": "All", - "value": [ - "$__all" - ] - }, - "datasource": "$datasource", - "definition": "label_values(grpc_server_requests_duration_ms_bucket{instance=~\"$instance\"},caller)", - "hide": 0, - "includeAll": true, - "label": "grpc依赖接口", - "multi": true, - "name": "grpc_caller", - "options": [], - "query": "label_values(grpc_server_requests_duration_ms_bucket{instance=~\"$instance\"},caller)", - "refresh": 1, - "regex": "", - "skipUrlSync": false, - "sort": 0, - "tagValuesQuery": "", - "tags": [], - "tagsQuery": "", - "type": "query", - "useTags": false - } - ] - }, - "time": { - "from": "now-30m", - "to": "now" - }, - "timepicker": { - "hidden": false, - "refresh_intervals": [ - "5m", - "15m", - "30m", - "1h", - "2h", - "1d" - ], - "time_options": [ - "5m", - "15m", - "1h", - "6h", - "12h", - "24h", - "2d", - "7d", - "30d" - ] - }, - "timezone": "browser", - "title": "Go业务监控", - "uid": "FitP_nDZz", - "version": 10 -} diff --git a/options.go b/options.go new file mode 100644 index 000000000..d5aa4c804 --- /dev/null +++ b/options.go @@ -0,0 +1,79 @@ +package kratos + +import ( + "context" + "os" + + "github.com/go-kratos/kratos/v2/log" + "github.com/go-kratos/kratos/v2/registry" + "github.com/go-kratos/kratos/v2/transport" +) + +// Option is an application option. +type Option func(o *options) + +// options is an application options. +type options struct { + id string + name string + version string + metadata map[string]string + endpoints []string + + ctx context.Context + sigs []os.Signal + + logger log.Logger + registry registry.Registry + servers []transport.Server +} + +// ID with service id. +func ID(id string) Option { + return func(o *options) { o.id = id } +} + +// Name with service name. +func Name(name string) Option { + return func(o *options) { o.name = name } +} + +// Version with service version. +func Version(version string) Option { + return func(o *options) { o.version = version } +} + +// Metadata with service metadata. +func Metadata(md map[string]string) Option { + return func(o *options) { o.metadata = md } +} + +// Endpoint with service endpoint. +func Endpoint(endpoints ...string) Option { + return func(o *options) { o.endpoints = endpoints } +} + +// Context with service context. +func Context(ctx context.Context) Option { + return func(o *options) { o.ctx = ctx } +} + +// Signal with exit signals. +func Signal(sigs ...os.Signal) Option { + return func(o *options) { o.sigs = sigs } +} + +// Logger with service logger. +func Logger(logger log.Logger) Option { + return func(o *options) { o.logger = logger } +} + +// Registry with service registry. +func Registry(r registry.Registry) Option { + return func(o *options) { o.registry = r } +} + +// Server with transport servers. +func Server(srv ...transport.Server) Option { + return func(o *options) { o.servers = srv } +} diff --git a/pkg/cache/memcache/ascii_conn.go b/pkg/cache/memcache/ascii_conn.go deleted file mode 100644 index a09b7ed3a..000000000 --- a/pkg/cache/memcache/ascii_conn.go +++ /dev/null @@ -1,262 +0,0 @@ -package memcache - -import ( - "bufio" - "bytes" - "context" - "fmt" - "io" - "net" - "strconv" - "strings" - "time" - - pkgerr "github.com/pkg/errors" -) - -var ( - crlf = []byte("\r\n") - space = []byte(" ") - replyOK = []byte("OK\r\n") - replyStored = []byte("STORED\r\n") - replyNotStored = []byte("NOT_STORED\r\n") - replyExists = []byte("EXISTS\r\n") - replyNotFound = []byte("NOT_FOUND\r\n") - replyDeleted = []byte("DELETED\r\n") - replyEnd = []byte("END\r\n") - replyTouched = []byte("TOUCHED\r\n") - replyClientErrorPrefix = []byte("CLIENT_ERROR ") - replyServerErrorPrefix = []byte("SERVER_ERROR ") -) - -var _ protocolConn = &asiiConn{} - -// asiiConn is the low-level implementation of Conn -type asiiConn struct { - err error - conn net.Conn - // Read & Write - readTimeout time.Duration - writeTimeout time.Duration - rw *bufio.ReadWriter -} - -func replyToError(line []byte) error { - switch { - case bytes.Equal(line, replyStored): - return nil - case bytes.Equal(line, replyOK): - return nil - case bytes.Equal(line, replyDeleted): - return nil - case bytes.Equal(line, replyTouched): - return nil - case bytes.Equal(line, replyNotStored): - return ErrNotStored - case bytes.Equal(line, replyExists): - return ErrCASConflict - case bytes.Equal(line, replyNotFound): - return ErrNotFound - case bytes.Equal(line, replyNotStored): - return ErrNotStored - case bytes.Equal(line, replyExists): - return ErrCASConflict - } - return pkgerr.WithStack(protocolError(string(line))) -} - -func (c *asiiConn) Populate(ctx context.Context, cmd string, key string, flags uint32, expiration int32, cas uint64, data []byte) error { - var err error - c.conn.SetWriteDeadline(shrinkDeadline(ctx, c.writeTimeout)) - // [noreply]\r\n - if cmd == "cas" { - _, err = fmt.Fprintf(c.rw, "%s %s %d %d %d %d\r\n", cmd, key, flags, expiration, len(data), cas) - } else { - _, err = fmt.Fprintf(c.rw, "%s %s %d %d %d\r\n", cmd, key, flags, expiration, len(data)) - } - if err != nil { - return c.fatal(err) - } - c.rw.Write(data) - c.rw.Write(crlf) - if err = c.rw.Flush(); err != nil { - return c.fatal(err) - } - c.conn.SetReadDeadline(shrinkDeadline(ctx, c.readTimeout)) - line, err := c.rw.ReadSlice('\n') - if err != nil { - return c.fatal(err) - } - return replyToError(line) -} - -// newConn returns a new memcache connection for the given net connection. -func newASCIIConn(netConn net.Conn, readTimeout, writeTimeout time.Duration) (protocolConn, error) { - if writeTimeout <= 0 || readTimeout <= 0 { - return nil, pkgerr.Errorf("readTimeout writeTimeout can't be zero") - } - c := &asiiConn{ - conn: netConn, - rw: bufio.NewReadWriter(bufio.NewReader(netConn), - bufio.NewWriter(netConn)), - readTimeout: readTimeout, - writeTimeout: writeTimeout, - } - return c, nil -} - -func (c *asiiConn) Close() error { - if c.err == nil { - c.err = pkgerr.New("memcache: closed") - } - return c.conn.Close() -} - -func (c *asiiConn) fatal(err error) error { - if c.err == nil { - c.err = pkgerr.WithStack(err) - // Close connection to force errors on subsequent calls and to unblock - // other reader or writer. - c.conn.Close() - } - return c.err -} - -func (c *asiiConn) Err() error { - return c.err -} - -func (c *asiiConn) Get(ctx context.Context, key string) (result *Item, err error) { - c.conn.SetWriteDeadline(shrinkDeadline(ctx, c.writeTimeout)) - if _, err = fmt.Fprintf(c.rw, "gets %s\r\n", key); err != nil { - return nil, c.fatal(err) - } - if err = c.rw.Flush(); err != nil { - return nil, c.fatal(err) - } - if err = c.parseGetReply(ctx, func(it *Item) { - result = it - }); err != nil { - return - } - if result == nil { - return nil, ErrNotFound - } - return -} - -func (c *asiiConn) GetMulti(ctx context.Context, keys ...string) (map[string]*Item, error) { - var err error - c.conn.SetWriteDeadline(shrinkDeadline(ctx, c.writeTimeout)) - if _, err = fmt.Fprintf(c.rw, "gets %s\r\n", strings.Join(keys, " ")); err != nil { - return nil, c.fatal(err) - } - if err = c.rw.Flush(); err != nil { - return nil, c.fatal(err) - } - results := make(map[string]*Item, len(keys)) - if err = c.parseGetReply(ctx, func(it *Item) { - results[it.Key] = it - }); err != nil { - return nil, err - } - return results, nil -} - -func (c *asiiConn) parseGetReply(ctx context.Context, f func(*Item)) error { - c.conn.SetReadDeadline(shrinkDeadline(ctx, c.readTimeout)) - for { - line, err := c.rw.ReadSlice('\n') - if err != nil { - return c.fatal(err) - } - if bytes.Equal(line, replyEnd) { - return nil - } - if bytes.HasPrefix(line, replyServerErrorPrefix) { - errMsg := line[len(replyServerErrorPrefix):] - return c.fatal(protocolError(errMsg)) - } - it := new(Item) - size, err := scanGetReply(line, it) - if err != nil { - return c.fatal(err) - } - it.Value = make([]byte, size+2) - if _, err = io.ReadFull(c.rw, it.Value); err != nil { - return c.fatal(err) - } - if !bytes.HasSuffix(it.Value, crlf) { - return c.fatal(protocolError("corrupt get reply, no except CRLF")) - } - it.Value = it.Value[:size] - f(it) - } -} - -func scanGetReply(line []byte, item *Item) (size int, err error) { - pattern := "VALUE %s %d %d %d\r\n" - dest := []interface{}{&item.Key, &item.Flags, &size, &item.cas} - if bytes.Count(line, space) == 3 { - pattern = "VALUE %s %d %d\r\n" - dest = dest[:3] - } - n, err := fmt.Sscanf(string(line), pattern, dest...) - if err != nil || n != len(dest) { - return -1, fmt.Errorf("memcache: unexpected line in get response: %q", line) - } - return size, nil -} - -func (c *asiiConn) Touch(ctx context.Context, key string, expire int32) error { - line, err := c.writeReadLine(ctx, "touch %s %d\r\n", key, expire) - if err != nil { - return err - } - return replyToError(line) -} - -func (c *asiiConn) IncrDecr(ctx context.Context, cmd, key string, delta uint64) (uint64, error) { - line, err := c.writeReadLine(ctx, "%s %s %d\r\n", cmd, key, delta) - if err != nil { - return 0, err - } - switch { - case bytes.Equal(line, replyNotFound): - return 0, ErrNotFound - case bytes.HasPrefix(line, replyClientErrorPrefix): - errMsg := line[len(replyClientErrorPrefix):] - return 0, pkgerr.WithStack(protocolError(errMsg)) - } - val, err := strconv.ParseUint(string(line[:len(line)-2]), 10, 64) - if err != nil { - return 0, err - } - return val, nil -} - -func (c *asiiConn) Delete(ctx context.Context, key string) error { - line, err := c.writeReadLine(ctx, "delete %s\r\n", key) - if err != nil { - return err - } - return replyToError(line) -} - -func (c *asiiConn) writeReadLine(ctx context.Context, format string, args ...interface{}) ([]byte, error) { - var err error - c.conn.SetWriteDeadline(shrinkDeadline(ctx, c.writeTimeout)) - _, err = fmt.Fprintf(c.rw, format, args...) - if err != nil { - return nil, c.fatal(pkgerr.WithStack(err)) - } - if err = c.rw.Flush(); err != nil { - return nil, c.fatal(pkgerr.WithStack(err)) - } - c.conn.SetReadDeadline(shrinkDeadline(ctx, c.readTimeout)) - line, err := c.rw.ReadSlice('\n') - if err != nil { - return line, c.fatal(pkgerr.WithStack(err)) - } - return line, nil -} diff --git a/pkg/cache/memcache/ascii_conn_test.go b/pkg/cache/memcache/ascii_conn_test.go deleted file mode 100644 index 32efa5c4b..000000000 --- a/pkg/cache/memcache/ascii_conn_test.go +++ /dev/null @@ -1,567 +0,0 @@ -package memcache - -import ( - "bytes" - "strconv" - "strings" - - "testing" -) - -func TestASCIIConnAdd(t *testing.T) { - tests := []struct { - name string - a *Item - e error - }{ - { - "Add", - &Item{ - Key: "test_add", - Value: []byte("0"), - Flags: 0, - Expiration: 60, - }, - nil, - }, - { - "Add_Large", - &Item{ - Key: "test_add_large", - Value: bytes.Repeat(space, _largeValue+1), - Flags: 0, - Expiration: 60, - }, - nil, - }, - { - "Add_Exist", - &Item{ - Key: "test_add", - Value: []byte("0"), - Flags: 0, - Expiration: 60, - }, - ErrNotStored, - }, - } - for _, test := range tests { - t.Run(test.name, func(t *testing.T) { - if err := testConnASCII.Add(test.a); err != test.e { - t.Fatal(err) - } - if b, err := testConnASCII.Get(test.a.Key); err != nil { - t.Fatal(err) - } else { - compareItem(t, test.a, b) - } - }) - } -} - -func TestASCIIConnGet(t *testing.T) { - tests := []struct { - name string - a *Item - k string - e error - }{ - { - "Get", - &Item{ - Key: "test_get", - Value: []byte("0"), - Flags: 0, - Expiration: 60, - }, - "test_get", - nil, - }, - { - "Get_NotExist", - &Item{ - Key: "test_get_not_exist", - Value: []byte("0"), - Flags: 0, - Expiration: 60, - }, - "test_get_not_exist!", - ErrNotFound, - }, - } - for _, test := range tests { - t.Run(test.name, func(t *testing.T) { - if err := testConnASCII.Add(test.a); err != nil { - t.Fatal(err) - } - if b, err := testConnASCII.Get(test.a.Key); err != nil { - t.Fatal(err) - } else { - compareItem(t, test.a, b) - } - }) - } -} - -//func TestGetHasErr(t *testing.T) { -// prepareEnv(t) -// -// st := &TestItem{Name: "json", Age: 10} -// itemx := &Item{Key: "test", Object: st, Flags: FlagJSON} -// c.Set(itemx) -// -// expected := errors.New("some error") -// monkey.Patch(scanGetReply, func(line []byte, item *Item) (size int, err error) { -// return 0, expected -// }) -// -// if _, err := c.Get("test"); err.Error() != expected.Error() { -// t.Errorf("conn.Get() unexpected error(%v)", err) -// } -// if err := c.(*asciiConn).err; err.Error() != expected.Error() { -// t.Errorf("unexpected error(%v)", err) -// } -//} - -func TestASCIIConnGetMulti(t *testing.T) { - tests := []struct { - name string - a []*Item - k []string - e error - }{ - {"getMulti_Add", - []*Item{ - { - Key: "get_multi_1", - Value: []byte("test"), - Flags: FlagRAW, - Expiration: 60, - cas: 0, - }, - { - Key: "get_multi_2", - Value: []byte("test2"), - Flags: FlagRAW, - Expiration: 60, - cas: 0, - }, - }, - []string{"get_multi_1", "get_multi_2"}, - nil, - }, - } - - for _, test := range tests { - t.Run(test.name, func(t *testing.T) { - for _, i := range test.a { - if err := testConnASCII.Set(i); err != nil { - t.Fatal(err) - } - } - if r, err := testConnASCII.GetMulti(test.k); err != nil { - t.Fatal(err) - } else { - reply := r["get_multi_1"] - compareItem(t, reply, test.a[0]) - reply = r["get_multi_2"] - compareItem(t, reply, test.a[1]) - } - }) - } -} - -func TestASCIIConnSet(t *testing.T) { - tests := []struct { - name string - a *Item - e error - }{ - { - "SetLowerBound", - &Item{ - Key: strings.Repeat("a", 1), - Value: []byte("4"), - Flags: 0, - Expiration: 60, - }, - nil, - }, - { - "SetUpperBound", - &Item{ - Key: strings.Repeat("a", 250), - Value: []byte("3"), - Flags: 0, - Expiration: 60, - }, - nil, - }, - { - "SetIllegalKeyZeroLength", - &Item{ - Key: "", - Value: []byte("2"), - Flags: 0, - Expiration: 60, - }, - ErrMalformedKey, - }, - { - "SetIllegalKeyLengthExceededLimit", - &Item{ - Key: " ", - Value: []byte("1"), - Flags: 0, - Expiration: 60, - }, - ErrMalformedKey, - }, - { - "SeJsonItem", - &Item{ - Key: "set_obj", - Object: &struct { - Name string - Age int - }{"json", 10}, - Expiration: 60, - Flags: FlagJSON, - }, - nil, - }, - { - "SeErrItemJSONGzip", - &Item{ - Key: "set_err_item", - Expiration: 60, - Flags: FlagJSON | FlagGzip, - }, - ErrItem, - }, - { - "SeErrItemBytesValueWrongFlag", - &Item{ - Key: "set_err_item", - Value: []byte("2"), - Expiration: 60, - Flags: FlagJSON, - }, - ErrItem, - }, - } - - for _, test := range tests { - t.Run(test.name, func(t *testing.T) { - if err := testConnASCII.Set(test.a); err != test.e { - t.Fatal(err) - } - }) - } -} - -func TestASCIIConnCompareAndSwap(t *testing.T) { - tests := []struct { - name string - a *Item - b *Item - c *Item - k string - e error - }{ - { - "CompareAndSwap", - &Item{ - Key: "test_cas", - Value: []byte("2"), - Flags: 0, - Expiration: 60, - }, - nil, - &Item{ - Key: "test_cas", - Value: []byte("3"), - Flags: 0, - Expiration: 60, - }, - "test_cas", - nil, - }, - { - "CompareAndSwapErrCASConflict", - &Item{ - Key: "test_cas_conflict", - Value: []byte("2"), - Flags: 0, - Expiration: 60, - }, - &Item{ - Key: "test_cas_conflict", - Value: []byte("1"), - Flags: 0, - Expiration: 60, - }, - &Item{ - Key: "test_cas_conflict", - Value: []byte("3"), - Flags: 0, - Expiration: 60, - }, - "test_cas_conflict", - ErrCASConflict, - }, - } - - for _, test := range tests { - t.Run(test.name, func(t *testing.T) { - if err := testConnASCII.Set(test.a); err != nil { - t.Fatal(err) - } - r, err := testConnASCII.Get(test.k) - if err != nil { - t.Fatal(err) - } - - if test.b != nil { - if err := testConnASCII.Set(test.b); err != nil { - t.Fatal(err) - } - } - - r.Value = test.c.Value - if err := testConnASCII.CompareAndSwap(r); err != nil { - if err != test.e { - t.Fatal(err) - } - } else { - if fr, err := testConnASCII.Get(test.k); err != nil { - t.Fatal(err) - } else { - compareItem(t, fr, test.c) - } - } - }) - } - - t.Run("TestCompareAndSwapErrNotFound", func(t *testing.T) { - ti := &Item{ - Key: "test_cas_notfound", - Value: []byte("2"), - Flags: 0, - Expiration: 60, - } - if err := testConnASCII.Set(ti); err != nil { - t.Fatal(err) - } - r, err := testConnASCII.Get(ti.Key) - if err != nil { - t.Fatal(err) - } - - r.Key = "test_cas_notfound_boom" - r.Value = []byte("3") - if err := testConnASCII.CompareAndSwap(r); err != nil { - if err != ErrNotFound { - t.Fatal(err) - } - } - }) -} - -func TestASCIIConnReplace(t *testing.T) { - tests := []struct { - name string - a *Item - b *Item - e error - }{ - { - "TestReplace", - &Item{ - Key: "test_replace", - Value: []byte("2"), - Flags: 0, - Expiration: 60, - }, - &Item{ - Key: "test_replace", - Value: []byte("3"), - Flags: 0, - Expiration: 60, - }, - nil, - }, - { - "TestReplaceErrNotStored", - &Item{ - Key: "test_replace_not_stored", - Value: []byte("2"), - Flags: 0, - Expiration: 60, - }, - &Item{ - Key: "test_replace_not_stored_boom", - Value: []byte("3"), - Flags: 0, - Expiration: 60, - }, - ErrNotStored, - }, - } - - for _, test := range tests { - t.Run(test.name, func(t *testing.T) { - if err := testConnASCII.Set(test.a); err != nil { - t.Fatal(err) - } - if err := testConnASCII.Replace(test.b); err != nil { - if err == test.e { - return - } - t.Fatal(err) - } - if r, err := testConnASCII.Get(test.b.Key); err != nil { - t.Fatal(err) - } else { - compareItem(t, r, test.b) - } - }) - } -} - -func TestASCIIConnIncrDecr(t *testing.T) { - tests := []struct { - fn func(key string, delta uint64) (uint64, error) - name string - k string - v uint64 - w uint64 - }{ - { - testConnASCII.Increment, - "Incr_10", - "test_incr", - 10, - 10, - }, - { - testConnASCII.Increment, - "Incr_10(2)", - "test_incr", - 10, - 20, - }, - { - testConnASCII.Decrement, - "Decr_10", - "test_incr", - 10, - 10, - }, - } - if err := testConnASCII.Add(&Item{ - Key: "test_incr", - Value: []byte("0"), - }); err != nil { - t.Fatal(err) - } - for _, test := range tests { - t.Run(test.name, func(t *testing.T) { - if a, err := test.fn(test.k, test.v); err != nil { - t.Fatal(err) - } else { - if a != test.w { - t.Fatalf("want %d, got %d", test.w, a) - } - } - if b, err := testConnASCII.Get(test.k); err != nil { - t.Fatal(err) - } else { - if string(b.Value) != strconv.FormatUint(test.w, 10) { - t.Fatalf("want %s, got %d", b.Value, test.w) - } - } - }) - } -} - -func TestASCIIConnTouch(t *testing.T) { - tests := []struct { - name string - k string - a *Item - e error - }{ - { - "Touch", - "test_touch", - &Item{ - Key: "test_touch", - Value: []byte("0"), - Expiration: 60, - }, - nil, - }, - { - "Touch_NotExist", - "test_touch_not_exist", - nil, - ErrNotFound, - }, - } - for _, test := range tests { - if test.a != nil { - if err := testConnASCII.Add(test.a); err != nil { - t.Fatal(err) - } - if err := testConnASCII.Touch(test.k, 1); err != test.e { - t.Fatal(err) - } - } - } -} - -func TestASCIIConnDelete(t *testing.T) { - tests := []struct { - name string - k string - a *Item - e error - }{ - { - "Delete", - "test_delete", - &Item{ - Key: "test_delete", - Value: []byte("0"), - Expiration: 60, - }, - nil, - }, - { - "Delete_NotExist", - "test_delete_not_exist", - nil, - ErrNotFound, - }, - } - for _, test := range tests { - if test.a != nil { - if err := testConnASCII.Add(test.a); err != nil { - t.Fatal(err) - } - if err := testConnASCII.Delete(test.k); err != test.e { - t.Fatal(err) - } - if _, err := testConnASCII.Get(test.k); err != ErrNotFound { - t.Fatal(err) - } - } - } -} - -func compareItem(t *testing.T, a, b *Item) { - if a.Key != b.Key || !bytes.Equal(a.Value, b.Value) || a.Flags != b.Flags { - t.Fatalf("compareItem: a(%s, %d, %d) : b(%s, %d, %d)", a.Key, len(a.Value), a.Flags, b.Key, len(b.Value), b.Flags) - } -} diff --git a/pkg/cache/memcache/conn.go b/pkg/cache/memcache/conn.go deleted file mode 100644 index 4637e0815..000000000 --- a/pkg/cache/memcache/conn.go +++ /dev/null @@ -1,287 +0,0 @@ -package memcache - -import ( - "context" - "fmt" - "net" - "strconv" - "time" - - pkgerr "github.com/pkg/errors" -) - -const ( - // 1024*1024 - 1, set error??? - _largeValue = 1000 * 1000 // 1MB -) - -// low level connection that implement memcache protocol provide basic operation. -type protocolConn interface { - Populate(ctx context.Context, cmd string, key string, flags uint32, expiration int32, cas uint64, data []byte) error - Get(ctx context.Context, key string) (*Item, error) - GetMulti(ctx context.Context, keys ...string) (map[string]*Item, error) - Touch(ctx context.Context, key string, expire int32) error - IncrDecr(ctx context.Context, cmd, key string, delta uint64) (uint64, error) - Delete(ctx context.Context, key string) error - Close() error - Err() error -} - -// DialOption specifies an option for dialing a Memcache server. -type DialOption struct { - f func(*dialOptions) -} - -type dialOptions struct { - readTimeout time.Duration - writeTimeout time.Duration - protocol string - dial func(network, addr string) (net.Conn, error) -} - -// DialReadTimeout specifies the timeout for reading a single command reply. -func DialReadTimeout(d time.Duration) DialOption { - return DialOption{func(do *dialOptions) { - do.readTimeout = d - }} -} - -// DialWriteTimeout specifies the timeout for writing a single command. -func DialWriteTimeout(d time.Duration) DialOption { - return DialOption{func(do *dialOptions) { - do.writeTimeout = d - }} -} - -// DialConnectTimeout specifies the timeout for connecting to the Memcache server. -func DialConnectTimeout(d time.Duration) DialOption { - return DialOption{func(do *dialOptions) { - dialer := net.Dialer{Timeout: d} - do.dial = dialer.Dial - }} -} - -// DialNetDial specifies a custom dial function for creating TCP -// connections. If this option is left out, then net.Dial is -// used. DialNetDial overrides DialConnectTimeout. -func DialNetDial(dial func(network, addr string) (net.Conn, error)) DialOption { - return DialOption{func(do *dialOptions) { - do.dial = dial - }} -} - -// Dial connects to the Memcache server at the given network and -// address using the specified options. -func Dial(network, address string, options ...DialOption) (Conn, error) { - do := dialOptions{ - dial: net.Dial, - } - for _, option := range options { - option.f(&do) - } - netConn, err := do.dial(network, address) - if err != nil { - return nil, pkgerr.WithStack(err) - } - pconn, err := newASCIIConn(netConn, do.readTimeout, do.writeTimeout) - return &conn{pconn: pconn, ed: newEncodeDecoder()}, err -} - -type conn struct { - // low level connection. - pconn protocolConn - ed *encodeDecode -} - -func (c *conn) Close() error { - return c.pconn.Close() -} - -func (c *conn) Err() error { - return c.pconn.Err() -} - -func (c *conn) AddContext(ctx context.Context, item *Item) error { - return c.populate(ctx, "add", item) -} - -func (c *conn) SetContext(ctx context.Context, item *Item) error { - return c.populate(ctx, "set", item) -} - -func (c *conn) ReplaceContext(ctx context.Context, item *Item) error { - return c.populate(ctx, "replace", item) -} - -func (c *conn) CompareAndSwapContext(ctx context.Context, item *Item) error { - return c.populate(ctx, "cas", item) -} - -func (c *conn) populate(ctx context.Context, cmd string, item *Item) error { - if !legalKey(item.Key) { - return ErrMalformedKey - } - data, err := c.ed.encode(item) - if err != nil { - return err - } - length := len(data) - if length < _largeValue { - return c.pconn.Populate(ctx, cmd, item.Key, item.Flags, item.Expiration, item.cas, data) - } - count := length/_largeValue + 1 - if err = c.pconn.Populate(ctx, cmd, item.Key, item.Flags|flagLargeValue, item.Expiration, item.cas, []byte(strconv.Itoa(length))); err != nil { - return err - } - var chunk []byte - for i := 1; i <= count; i++ { - if i == count { - chunk = data[_largeValue*(count-1):] - } else { - chunk = data[_largeValue*(i-1) : _largeValue*i] - } - key := fmt.Sprintf("%s%d", item.Key, i) - if err = c.pconn.Populate(ctx, cmd, key, item.Flags, item.Expiration, item.cas, chunk); err != nil { - return err - } - } - return nil -} - -func (c *conn) GetContext(ctx context.Context, key string) (*Item, error) { - if !legalKey(key) { - return nil, ErrMalformedKey - } - result, err := c.pconn.Get(ctx, key) - if err != nil { - return nil, err - } - if result.Flags&flagLargeValue != flagLargeValue { - return result, err - } - return c.getLargeItem(ctx, result) -} - -func (c *conn) getLargeItem(ctx context.Context, result *Item) (*Item, error) { - length, err := strconv.Atoi(string(result.Value)) - if err != nil { - return nil, err - } - count := length/_largeValue + 1 - keys := make([]string, 0, count) - for i := 1; i <= count; i++ { - keys = append(keys, fmt.Sprintf("%s%d", result.Key, i)) - } - var results map[string]*Item - if results, err = c.pconn.GetMulti(ctx, keys...); err != nil { - return nil, err - } - if len(results) < count { - return nil, ErrNotFound - } - result.Value = make([]byte, 0, length) - for _, k := range keys { - ti := results[k] - if ti == nil || ti.Value == nil { - return nil, ErrNotFound - } - result.Value = append(result.Value, ti.Value...) - } - result.Flags = result.Flags ^ flagLargeValue - return result, nil -} - -func (c *conn) GetMultiContext(ctx context.Context, keys []string) (map[string]*Item, error) { - // TODO: move to protocolConn? - for _, key := range keys { - if !legalKey(key) { - return nil, ErrMalformedKey - } - } - results, err := c.pconn.GetMulti(ctx, keys...) - if err != nil { - return results, err - } - for k, v := range results { - if v.Flags&flagLargeValue != flagLargeValue { - continue - } - if v, err = c.getLargeItem(ctx, v); err != nil { - return results, err - } - results[k] = v - } - return results, nil -} - -func (c *conn) DeleteContext(ctx context.Context, key string) error { - if !legalKey(key) { - return ErrMalformedKey - } - return c.pconn.Delete(ctx, key) -} - -func (c *conn) IncrementContext(ctx context.Context, key string, delta uint64) (uint64, error) { - if !legalKey(key) { - return 0, ErrMalformedKey - } - return c.pconn.IncrDecr(ctx, "incr", key, delta) -} - -func (c *conn) DecrementContext(ctx context.Context, key string, delta uint64) (uint64, error) { - if !legalKey(key) { - return 0, ErrMalformedKey - } - return c.pconn.IncrDecr(ctx, "decr", key, delta) -} - -func (c *conn) TouchContext(ctx context.Context, key string, seconds int32) error { - if !legalKey(key) { - return ErrMalformedKey - } - return c.pconn.Touch(ctx, key, seconds) -} - -func (c *conn) Add(item *Item) error { - return c.AddContext(context.TODO(), item) -} - -func (c *conn) Set(item *Item) error { - return c.SetContext(context.TODO(), item) -} - -func (c *conn) Replace(item *Item) error { - return c.ReplaceContext(context.TODO(), item) -} - -func (c *conn) Get(key string) (*Item, error) { - return c.GetContext(context.TODO(), key) -} - -func (c *conn) GetMulti(keys []string) (map[string]*Item, error) { - return c.GetMultiContext(context.TODO(), keys) -} - -func (c *conn) Delete(key string) error { - return c.DeleteContext(context.TODO(), key) -} - -func (c *conn) Increment(key string, delta uint64) (newValue uint64, err error) { - return c.IncrementContext(context.TODO(), key, delta) -} - -func (c *conn) Decrement(key string, delta uint64) (newValue uint64, err error) { - return c.DecrementContext(context.TODO(), key, delta) -} - -func (c *conn) CompareAndSwap(item *Item) error { - return c.CompareAndSwapContext(context.TODO(), item) -} - -func (c *conn) Touch(key string, seconds int32) (err error) { - return c.TouchContext(context.TODO(), key, seconds) -} - -func (c *conn) Scan(item *Item, v interface{}) (err error) { - return pkgerr.WithStack(c.ed.decode(item, v)) -} diff --git a/pkg/cache/memcache/conn_test.go b/pkg/cache/memcache/conn_test.go deleted file mode 100644 index 789e52cfc..000000000 --- a/pkg/cache/memcache/conn_test.go +++ /dev/null @@ -1,185 +0,0 @@ -package memcache - -import ( - "bytes" - "encoding/json" - "testing" - - "github.com/gogo/protobuf/proto" - - test "github.com/go-kratos/kratos/pkg/cache/memcache/test" -) - -func TestConnRaw(t *testing.T) { - item := &Item{ - Key: "test", - Value: []byte("test"), - Flags: FlagRAW, - Expiration: 60, - cas: 0, - } - if err := testConnASCII.Set(item); err != nil { - t.Errorf("conn.Store() error(%v)", err) - } -} - -func TestConnSerialization(t *testing.T) { - type TestObj struct { - Name string - Age int32 - } - - tests := []struct { - name string - a *Item - e error - }{ - - { - "JSON", - &Item{ - Key: "test_json", - Object: &TestObj{"json", 1}, - Expiration: 60, - Flags: FlagJSON, - }, - nil, - }, - { - "JSONGzip", - &Item{ - Key: "test_json_gzip", - Object: &TestObj{"jsongzip", 2}, - Expiration: 60, - Flags: FlagJSON | FlagGzip, - }, - nil, - }, - { - "GOB", - &Item{ - Key: "test_gob", - Object: &TestObj{"gob", 3}, - Expiration: 60, - Flags: FlagGOB, - }, - nil, - }, - { - "GOBGzip", - &Item{ - Key: "test_gob_gzip", - Object: &TestObj{"gobgzip", 4}, - Expiration: 60, - Flags: FlagGOB | FlagGzip, - }, - nil, - }, - { - "Protobuf", - &Item{ - Key: "test_protobuf", - Object: &test.TestItem{Name: "protobuf", Age: 6}, - Expiration: 60, - Flags: FlagProtobuf, - }, - nil, - }, - { - "ProtobufGzip", - &Item{ - Key: "test_protobuf_gzip", - Object: &test.TestItem{Name: "protobufgzip", Age: 7}, - Expiration: 60, - Flags: FlagProtobuf | FlagGzip, - }, - nil, - }, - } - for _, tc := range tests { - t.Run(tc.name, func(t *testing.T) { - if err := testConnASCII.Set(tc.a); err != nil { - t.Fatal(err) - } - if r, err := testConnASCII.Get(tc.a.Key); err != tc.e { - t.Fatal(err) - } else { - if (tc.a.Flags & FlagProtobuf) > 0 { - var no test.TestItem - if err := testConnASCII.Scan(r, &no); err != nil { - t.Fatal(err) - } - if (tc.a.Object.(*test.TestItem).Name != no.Name) || (tc.a.Object.(*test.TestItem).Age != no.Age) { - t.Fatalf("compare failed error, %v %v", tc.a.Object.(*test.TestItem), no) - } - } else { - var no TestObj - if err := testConnASCII.Scan(r, &no); err != nil { - t.Fatal(err) - } - if (tc.a.Object.(*TestObj).Name != no.Name) || (tc.a.Object.(*TestObj).Age != no.Age) { - t.Fatalf("compare failed error, %v %v", tc.a.Object.(*TestObj), no) - } - } - } - }) - } -} - -func BenchmarkConnJSON(b *testing.B) { - st := &struct { - Name string - Age int - }{"json", 10} - itemx := &Item{Key: "json", Object: st, Flags: FlagJSON} - var ( - eb bytes.Buffer - je *json.Encoder - ir bytes.Reader - jd *json.Decoder - jr reader - nst test.TestItem - ) - jd = json.NewDecoder(&jr) - je = json.NewEncoder(&eb) - eb.Grow(_encodeBuf) - // NOTE reuse bytes.Buffer internal buf - // DON'T concurrency call Scan - b.ResetTimer() - for i := 0; i < b.N; i++ { - eb.Reset() - if err := je.Encode(itemx.Object); err != nil { - return - } - data := eb.Bytes() - ir.Reset(data) - jr.Reset(&ir) - jd.Decode(&nst) - } -} - -func BenchmarkConnProtobuf(b *testing.B) { - st := &test.TestItem{Name: "protobuf", Age: 10} - itemx := &Item{Key: "protobuf", Object: st, Flags: FlagJSON} - var ( - eb bytes.Buffer - nst test.TestItem - ped *proto.Buffer - ) - ped = proto.NewBuffer(eb.Bytes()) - eb.Grow(_encodeBuf) - b.ResetTimer() - for i := 0; i < b.N; i++ { - ped.Reset() - pb, ok := itemx.Object.(proto.Message) - if !ok { - return - } - if err := ped.Marshal(pb); err != nil { - return - } - data := ped.Bytes() - ped.SetBuf(data) - ped.Unmarshal(&nst) - } -} diff --git a/pkg/cache/memcache/encoding.go b/pkg/cache/memcache/encoding.go deleted file mode 100644 index 1a386af9b..000000000 --- a/pkg/cache/memcache/encoding.go +++ /dev/null @@ -1,162 +0,0 @@ -package memcache - -import ( - "bytes" - "compress/gzip" - "encoding/gob" - "encoding/json" - "io" - - "github.com/gogo/protobuf/proto" -) - -type reader struct { - io.Reader -} - -func (r *reader) Reset(rd io.Reader) { - r.Reader = rd -} - -const _encodeBuf = 4096 // 4kb - -type encodeDecode struct { - // Item Reader - ir bytes.Reader - // Compress - gr gzip.Reader - gw *gzip.Writer - cb bytes.Buffer - // Encoding - edb bytes.Buffer - // json - jr reader - jd *json.Decoder - je *json.Encoder - // protobuffer - ped *proto.Buffer -} - -func newEncodeDecoder() *encodeDecode { - ed := &encodeDecode{} - ed.jd = json.NewDecoder(&ed.jr) - ed.je = json.NewEncoder(&ed.edb) - ed.gw = gzip.NewWriter(&ed.cb) - ed.edb.Grow(_encodeBuf) - // NOTE reuse bytes.Buffer internal buf - // DON'T concurrency call Scan - ed.ped = proto.NewBuffer(ed.edb.Bytes()) - return ed -} - -func (ed *encodeDecode) encode(item *Item) (data []byte, err error) { - if (item.Flags | _flagEncoding) == _flagEncoding { - if item.Value == nil { - return nil, ErrItem - } - } else if item.Object == nil { - return nil, ErrItem - } - // encoding - switch { - case item.Flags&FlagGOB == FlagGOB: - ed.edb.Reset() - if err = gob.NewEncoder(&ed.edb).Encode(item.Object); err != nil { - return - } - data = ed.edb.Bytes() - case item.Flags&FlagProtobuf == FlagProtobuf: - ed.edb.Reset() - ed.ped.SetBuf(ed.edb.Bytes()) - pb, ok := item.Object.(proto.Message) - if !ok { - err = ErrItemObject - return - } - if err = ed.ped.Marshal(pb); err != nil { - return - } - data = ed.ped.Bytes() - case item.Flags&FlagJSON == FlagJSON: - ed.edb.Reset() - if err = ed.je.Encode(item.Object); err != nil { - return - } - data = ed.edb.Bytes() - default: - data = item.Value - } - // compress - if item.Flags&FlagGzip == FlagGzip { - ed.cb.Reset() - ed.gw.Reset(&ed.cb) - if _, err = ed.gw.Write(data); err != nil { - return - } - if err = ed.gw.Close(); err != nil { - return - } - data = ed.cb.Bytes() - } - if len(data) > 8000000 { - err = ErrValueSize - } - return -} - -func (ed *encodeDecode) decode(item *Item, v interface{}) (err error) { - var ( - data []byte - rd io.Reader - ) - ed.ir.Reset(item.Value) - rd = &ed.ir - if item.Flags&FlagGzip == FlagGzip { - rd = &ed.gr - if err = ed.gr.Reset(&ed.ir); err != nil { - return - } - defer func() { - if e := ed.gr.Close(); e != nil { - err = e - } - }() - } - switch { - case item.Flags&FlagGOB == FlagGOB: - err = gob.NewDecoder(rd).Decode(v) - case item.Flags&FlagJSON == FlagJSON: - ed.jr.Reset(rd) - err = ed.jd.Decode(v) - default: - data = item.Value - if item.Flags&FlagGzip == FlagGzip { - ed.edb.Reset() - if _, err = io.Copy(&ed.edb, rd); err != nil { - return - } - data = ed.edb.Bytes() - } - if item.Flags&FlagProtobuf == FlagProtobuf { - m, ok := v.(proto.Message) - if !ok { - err = ErrItemObject - return - } - ed.ped.SetBuf(data) - err = ed.ped.Unmarshal(m) - } else { - switch v.(type) { - case *[]byte: - d := v.(*[]byte) - *d = data - case *string: - d := v.(*string) - *d = string(data) - case interface{}: - err = json.Unmarshal(data, v) - } - } - } - return -} diff --git a/pkg/cache/memcache/encoding_test.go b/pkg/cache/memcache/encoding_test.go deleted file mode 100644 index c00a45c95..000000000 --- a/pkg/cache/memcache/encoding_test.go +++ /dev/null @@ -1,220 +0,0 @@ -package memcache - -import ( - "bytes" - "testing" - - mt "github.com/go-kratos/kratos/pkg/cache/memcache/test" -) - -func TestEncode(t *testing.T) { - type TestObj struct { - Name string - Age int32 - } - testObj := TestObj{"abc", 1} - - ed := newEncodeDecoder() - tests := []struct { - name string - a *Item - r []byte - e error - }{ - { - "EncodeRawFlagErrItem", - &Item{ - Object: &TestObj{"abc", 1}, - Flags: FlagRAW, - }, - []byte{}, - ErrItem, - }, - { - "EncodeEncodeFlagErrItem", - &Item{ - Value: []byte("test"), - Flags: FlagJSON, - }, - []byte{}, - ErrItem, - }, - { - "EncodeEmpty", - &Item{ - Value: []byte(""), - Flags: FlagRAW, - }, - []byte(""), - nil, - }, - { - "EncodeMaxSize", - &Item{ - Value: bytes.Repeat([]byte("A"), 8000000), - Flags: FlagRAW, - }, - bytes.Repeat([]byte("A"), 8000000), - nil, - }, - { - "EncodeExceededMaxSize", - &Item{ - Value: bytes.Repeat([]byte("A"), 8000000+1), - Flags: FlagRAW, - }, - nil, - ErrValueSize, - }, - { - "EncodeGOB", - &Item{ - Object: testObj, - Flags: FlagGOB, - }, - []byte{38, 255, 131, 3, 1, 1, 7, 84, 101, 115, 116, 79, 98, 106, 1, 255, 132, 0, 1, 2, 1, 4, 78, 97, 109, 101, 1, 12, 0, 1, 3, 65, 103, 101, 1, 4, 0, 0, 0, 10, 255, 132, 1, 3, 97, 98, 99, 1, 2, 0}, - nil, - }, - { - "EncodeJSON", - &Item{ - Object: testObj, - Flags: FlagJSON, - }, - []byte{123, 34, 78, 97, 109, 101, 34, 58, 34, 97, 98, 99, 34, 44, 34, 65, 103, 101, 34, 58, 49, 125, 10}, - nil, - }, - { - "EncodeProtobuf", - &Item{ - Object: &mt.TestItem{Name: "abc", Age: 1}, - Flags: FlagProtobuf, - }, - []byte{10, 3, 97, 98, 99, 16, 1}, - nil, - }, - { - "EncodeGzip", - &Item{ - Value: bytes.Repeat([]byte("B"), 50), - Flags: FlagGzip, - }, - []byte{31, 139, 8, 0, 0, 0, 0, 0, 0, 255, 114, 34, 25, 0, 2, 0, 0, 255, 255, 252, 253, 67, 209, 50, 0, 0, 0}, - nil, - }, - { - "EncodeGOBGzip", - &Item{ - Object: testObj, - Flags: FlagGOB | FlagGzip, - }, - []byte{31, 139, 8, 0, 0, 0, 0, 0, 0, 255, 82, 251, 223, 204, 204, 200, 200, 30, 146, 90, 92, 226, 159, 148, 197, 248, 191, 133, 129, 145, 137, 145, 197, 47, 49, 55, 149, 145, 135, 129, 145, 217, 49, 61, 149, 145, 133, 129, 129, 129, 235, 127, 11, 35, 115, 98, 82, 50, 35, 19, 3, 32, 0, 0, 255, 255, 211, 249, 1, 154, 50, 0, 0, 0}, - nil, - }, - } - for _, test := range tests { - t.Run(test.name, func(t *testing.T) { - if r, err := ed.encode(test.a); err != test.e { - t.Fatal(err) - } else { - if err == nil { - if !bytes.Equal(r, test.r) { - t.Fatalf("not equal, expect %v\n got %v", test.r, r) - } - } - } - }) - } -} - -func TestDecode(t *testing.T) { - type TestObj struct { - Name string - Age int32 - } - testObj := &TestObj{"abc", 1} - - ed := newEncodeDecoder() - tests := []struct { - name string - a *Item - r interface{} - e error - }{ - { - "DecodeGOB", - &Item{ - Flags: FlagGOB, - Value: []byte{38, 255, 131, 3, 1, 1, 7, 84, 101, 115, 116, 79, 98, 106, 1, 255, 132, 0, 1, 2, 1, 4, 78, 97, 109, 101, 1, 12, 0, 1, 3, 65, 103, 101, 1, 4, 0, 0, 0, 10, 255, 132, 1, 3, 97, 98, 99, 1, 2, 0}, - }, - testObj, - nil, - }, - { - "DecodeJSON", - &Item{ - Value: []byte{123, 34, 78, 97, 109, 101, 34, 58, 34, 97, 98, 99, 34, 44, 34, 65, 103, 101, 34, 58, 49, 125, 10}, - Flags: FlagJSON, - }, - testObj, - nil, - }, - { - "DecodeProtobuf", - &Item{ - Value: []byte{10, 3, 97, 98, 99, 16, 1}, - - Flags: FlagProtobuf, - }, - &mt.TestItem{Name: "abc", Age: 1}, - nil, - }, - { - "DecodeGzip", - &Item{ - Value: []byte{31, 139, 8, 0, 0, 0, 0, 0, 0, 255, 114, 34, 25, 0, 2, 0, 0, 255, 255, 252, 253, 67, 209, 50, 0, 0, 0}, - Flags: FlagGzip, - }, - bytes.Repeat([]byte("B"), 50), - nil, - }, - { - "DecodeGOBGzip", - &Item{ - Value: []byte{31, 139, 8, 0, 0, 0, 0, 0, 0, 255, 82, 251, 223, 204, 204, 200, 200, 30, 146, 90, 92, 226, 159, 148, 197, 248, 191, 133, 129, 145, 137, 145, 197, 47, 49, 55, 149, 145, 135, 129, 145, 217, 49, 61, 149, 145, 133, 129, 129, 129, 235, 127, 11, 35, 115, 98, 82, 50, 35, 19, 3, 32, 0, 0, 255, 255, 211, 249, 1, 154, 50, 0, 0, 0}, - Flags: FlagGOB | FlagGzip, - }, - testObj, - nil, - }, - } - for _, test := range tests { - t.Run(test.name, func(t *testing.T) { - if (test.a.Flags & FlagProtobuf) > 0 { - var dd mt.TestItem - if err := ed.decode(test.a, &dd); err != nil { - t.Fatal(err) - } - if (test.r.(*mt.TestItem).Name != dd.Name) || (test.r.(*mt.TestItem).Age != dd.Age) { - t.Fatalf("compare failed error, expect %v\n got %v", test.r.(*mt.TestItem), dd) - } - } else if test.a.Flags == FlagGzip { - var dd []byte - if err := ed.decode(test.a, &dd); err != nil { - t.Fatal(err) - } - if !bytes.Equal(dd, test.r.([]byte)) { - t.Fatalf("compare failed error, expect %v\n got %v", test.r, dd) - } - } else { - var dd TestObj - if err := ed.decode(test.a, &dd); err != nil { - t.Fatal(err) - } - if (test.r.(*TestObj).Name != dd.Name) || (test.r.(*TestObj).Age != dd.Age) { - t.Fatalf("compare failed error, expect %v\n got %v", test.r.(*TestObj), dd) - } - } - }) - } -} diff --git a/pkg/cache/memcache/errors.go b/pkg/cache/memcache/errors.go deleted file mode 100644 index db43bcfe4..000000000 --- a/pkg/cache/memcache/errors.go +++ /dev/null @@ -1,79 +0,0 @@ -package memcache - -import ( - "errors" - "fmt" - "strings" - - pkgerr "github.com/pkg/errors" -) - -var ( - // ErrNotFound not found - ErrNotFound = errors.New("memcache: key not found") - // ErrExists exists - ErrExists = errors.New("memcache: key exists") - // ErrNotStored not stored - ErrNotStored = errors.New("memcache: key not stored") - // ErrCASConflict means that a CompareAndSwap call failed due to the - // cached value being modified between the Get and the CompareAndSwap. - // If the cached value was simply evicted rather than replaced, - // ErrNotStored will be returned instead. - ErrCASConflict = errors.New("memcache: compare-and-swap conflict") - - // ErrPoolExhausted is returned from a pool connection method (Store, Get, - // Delete, IncrDecr, Err) when the maximum number of database connections - // in the pool has been reached. - ErrPoolExhausted = errors.New("memcache: connection pool exhausted") - // ErrPoolClosed pool closed - ErrPoolClosed = errors.New("memcache: connection pool closed") - // ErrConnClosed conn closed - ErrConnClosed = errors.New("memcache: connection closed") - // ErrMalformedKey is returned when an invalid key is used. - // Keys must be at maximum 250 bytes long and not - // contain whitespace or control characters. - ErrMalformedKey = errors.New("memcache: malformed key is too long or contains invalid characters") - // ErrValueSize item value size must less than 1mb - ErrValueSize = errors.New("memcache: item value size must not greater than 1mb") - // ErrStat stat error for monitor - ErrStat = errors.New("memcache unexpected errors") - // ErrItem item nil. - ErrItem = errors.New("memcache: item object nil") - // ErrItemObject object type Assertion failed - ErrItemObject = errors.New("memcache: item object protobuf type assertion failed") -) - -type protocolError string - -func (pe protocolError) Error() string { - return fmt.Sprintf("memcache: %s (possible server error or unsupported concurrent read by application)", string(pe)) -} - -func (pc *poolConn) formatErr(err error) string { - e := pkgerr.Cause(err) - switch e { - case ErrNotFound, ErrExists, ErrNotStored, nil: - if e == ErrNotFound { - _metricMisses.Inc(pc.p.c.Name, pc.p.c.Addr) - } - return "" - default: - es := e.Error() - switch { - case strings.HasPrefix(es, "read"): - return "read timeout" - case strings.HasPrefix(es, "dial"): - return "dial timeout" - case strings.HasPrefix(es, "write"): - return "write timeout" - case strings.Contains(es, "EOF"): - return "eof" - case strings.Contains(es, "reset"): - return "reset" - case strings.Contains(es, "broken"): - return "broken pipe" - default: - return "unexpected err" - } - } -} diff --git a/pkg/cache/memcache/example_test.go b/pkg/cache/memcache/example_test.go deleted file mode 100644 index bab5d1f00..000000000 --- a/pkg/cache/memcache/example_test.go +++ /dev/null @@ -1,177 +0,0 @@ -package memcache - -import ( - "encoding/json" - "fmt" - "time" -) - -var testExampleAddr string - -func ExampleConn_set() { - var ( - err error - value []byte - conn Conn - expire int32 = 100 - p = struct { - Name string - Age int64 - }{"golang", 10} - ) - cnop := DialConnectTimeout(time.Duration(time.Second)) - rdop := DialReadTimeout(time.Duration(time.Second)) - wrop := DialWriteTimeout(time.Duration(time.Second)) - if value, err = json.Marshal(p); err != nil { - fmt.Println(err) - return - } - if conn, err = Dial("tcp", testExampleAddr, cnop, rdop, wrop); err != nil { - fmt.Println(err) - return - } - // FlagRAW test - itemRaw := &Item{ - Key: "test_raw", - Value: value, - Expiration: expire, - } - if err = conn.Set(itemRaw); err != nil { - fmt.Println(err) - return - } - // FlagGzip - itemGZip := &Item{ - Key: "test_gzip", - Value: value, - Flags: FlagGzip, - Expiration: expire, - } - if err = conn.Set(itemGZip); err != nil { - fmt.Println(err) - return - } - // FlagGOB - itemGOB := &Item{ - Key: "test_gob", - Object: p, - Flags: FlagGOB, - Expiration: expire, - } - if err = conn.Set(itemGOB); err != nil { - fmt.Println(err) - return - } - // FlagJSON - itemJSON := &Item{ - Key: "test_json", - Object: p, - Flags: FlagJSON, - Expiration: expire, - } - if err = conn.Set(itemJSON); err != nil { - fmt.Println(err) - return - } - // FlagJSON | FlagGzip - itemJSONGzip := &Item{ - Key: "test_jsonGzip", - Object: p, - Flags: FlagJSON | FlagGzip, - Expiration: expire, - } - if err = conn.Set(itemJSONGzip); err != nil { - fmt.Println(err) - return - } - // Output: -} - -func ExampleConn_get() { - var ( - err error - item2 *Item - conn Conn - p struct { - Name string - Age int64 - } - ) - cnop := DialConnectTimeout(time.Duration(time.Second)) - rdop := DialReadTimeout(time.Duration(time.Second)) - wrop := DialWriteTimeout(time.Duration(time.Second)) - if conn, err = Dial("tcp", testExampleAddr, cnop, rdop, wrop); err != nil { - fmt.Println(err) - return - } - if item2, err = conn.Get("test_raw"); err != nil { - fmt.Println(err) - } else { - if err = conn.Scan(item2, &p); err != nil { - fmt.Printf("FlagRAW conn.Scan error(%v)\n", err) - return - } - } - // FlagGZip - if item2, err = conn.Get("test_gzip"); err != nil { - fmt.Println(err) - } else { - if err = conn.Scan(item2, &p); err != nil { - fmt.Printf("FlagGZip conn.Scan error(%v)\n", err) - return - } - } - // FlagGOB - if item2, err = conn.Get("test_gob"); err != nil { - fmt.Println(err) - } else { - if err = conn.Scan(item2, &p); err != nil { - fmt.Printf("FlagGOB conn.Scan error(%v)\n", err) - return - } - } - // FlagJSON - if item2, err = conn.Get("test_json"); err != nil { - fmt.Println(err) - } else { - if err = conn.Scan(item2, &p); err != nil { - fmt.Printf("FlagJSON conn.Scan error(%v)\n", err) - return - } - } - // Output: -} - -func ExampleConn_getMulti() { - var ( - err error - conn Conn - res map[string]*Item - keys = []string{"test_raw", "test_gzip"} - p struct { - Name string - Age int64 - } - ) - cnop := DialConnectTimeout(time.Duration(time.Second)) - rdop := DialReadTimeout(time.Duration(time.Second)) - wrop := DialWriteTimeout(time.Duration(time.Second)) - if conn, err = Dial("tcp", testExampleAddr, cnop, rdop, wrop); err != nil { - fmt.Println(err) - return - } - if res, err = conn.GetMulti(keys); err != nil { - fmt.Printf("conn.GetMulti(%v) error(%v)", keys, err) - return - } - for _, v := range res { - if err = conn.Scan(v, &p); err != nil { - fmt.Printf("conn.Scan error(%v)\n", err) - return - } - fmt.Println(p) - } - // Output: - //{golang 10} - //{golang 10} -} diff --git a/pkg/cache/memcache/main_test.go b/pkg/cache/memcache/main_test.go deleted file mode 100644 index 46e8b5689..000000000 --- a/pkg/cache/memcache/main_test.go +++ /dev/null @@ -1,85 +0,0 @@ -package memcache - -import ( - "log" - "os" - "testing" - "time" - - "github.com/go-kratos/kratos/pkg/container/pool" - xtime "github.com/go-kratos/kratos/pkg/time" -) - -var testConnASCII Conn -var testMemcache *Memcache -var testPool *Pool -var testMemcacheAddr string - -func setupTestConnASCII(addr string) { - var err error - cnop := DialConnectTimeout(time.Duration(2 * time.Second)) - rdop := DialReadTimeout(time.Duration(2 * time.Second)) - wrop := DialWriteTimeout(time.Duration(2 * time.Second)) - testConnASCII, err = Dial("tcp", addr, cnop, rdop, wrop) - if err != nil { - log.Fatal(err) - } - testConnASCII.Delete("test") - testConnASCII.Delete("test1") - testConnASCII.Delete("test2") - if err != nil { - log.Fatal(err) - } -} - -func setupTestMemcache(addr string) { - testConfig := &Config{ - Config: &pool.Config{ - Active: 10, - Idle: 10, - IdleTimeout: xtime.Duration(time.Second), - WaitTimeout: xtime.Duration(time.Second), - Wait: false, - }, - Addr: addr, - Proto: "tcp", - DialTimeout: xtime.Duration(time.Second), - ReadTimeout: xtime.Duration(time.Second), - WriteTimeout: xtime.Duration(time.Second), - } - testMemcache = New(testConfig) -} - -func setupTestPool(addr string) { - config := &Config{ - Name: "test", - Proto: "tcp", - Addr: addr, - DialTimeout: xtime.Duration(time.Second), - ReadTimeout: xtime.Duration(time.Second), - WriteTimeout: xtime.Duration(time.Second), - } - config.Config = &pool.Config{ - Active: 10, - Idle: 5, - IdleTimeout: xtime.Duration(90 * time.Second), - } - testPool = NewPool(config) -} - -func TestMain(m *testing.M) { - testMemcacheAddr = os.Getenv("TEST_MEMCACHE_ADDR") - if testExampleAddr == "" { - log.Print("TEST_MEMCACHE_ADDR not provide skip test.") - // ignored test. - os.Exit(0) - } - setupTestConnASCII(testMemcacheAddr) - setupTestMemcache(testMemcacheAddr) - setupTestPool(testMemcacheAddr) - // TODO: add setupexample? - testExampleAddr = testMemcacheAddr - - ret := m.Run() - os.Exit(ret) -} diff --git a/pkg/cache/memcache/memcache.go b/pkg/cache/memcache/memcache.go deleted file mode 100644 index ce51a0720..000000000 --- a/pkg/cache/memcache/memcache.go +++ /dev/null @@ -1,377 +0,0 @@ -package memcache - -import ( - "context" - - "github.com/go-kratos/kratos/pkg/container/pool" - xtime "github.com/go-kratos/kratos/pkg/time" -) - -const ( - // Flag, 15(encoding) bit+ 17(compress) bit - - // FlagRAW default flag. - FlagRAW = uint32(0) - // FlagGOB gob encoding. - FlagGOB = uint32(1) << 0 - // FlagJSON json encoding. - FlagJSON = uint32(1) << 1 - // FlagProtobuf protobuf - FlagProtobuf = uint32(1) << 2 - - _flagEncoding = uint32(0xFFFF8000) - - // FlagGzip gzip compress. - FlagGzip = uint32(1) << 15 - - // left mv 31??? not work!!! - flagLargeValue = uint32(1) << 30 -) - -// Item is an reply to be got or stored in a memcached server. -type Item struct { - // Key is the Item's key (250 bytes maximum). - Key string - - // Value is the Item's value. - Value []byte - - // Object is the Item's object for use codec. - Object interface{} - - // Flags are server-opaque flags whose semantics are entirely - // up to the app. - Flags uint32 - - // Expiration is the cache expiration time, in seconds: either a relative - // time from now (up to 1 month), or an absolute Unix epoch time. - // Zero means the Item has no expiration time. - Expiration int32 - - // Compare and swap ID. - cas uint64 -} - -// Conn represents a connection to a Memcache server. -// Command Reference: https://github.com/memcached/memcached/wiki/Commands -type Conn interface { - // Close closes the connection. - Close() error - - // Err returns a non-nil value if the connection is broken. The returned - // value is either the first non-nil value returned from the underlying - // network connection or a protocol parsing error. Applications should - // close broken connections. - Err() error - - // Add writes the given item, if no value already exists for its key. - // ErrNotStored is returned if that condition is not met. - Add(item *Item) error - - // Set writes the given item, unconditionally. - Set(item *Item) error - - // Replace writes the given item, but only if the server *does* already - // hold data for this key. - Replace(item *Item) error - - // Get sends a command to the server for gets data. - Get(key string) (*Item, error) - - // GetMulti is a batch version of Get. The returned map from keys to items - // may have fewer elements than the input slice, due to memcache cache - // misses. Each key must be at most 250 bytes in length. - // If no error is returned, the returned map will also be non-nil. - GetMulti(keys []string) (map[string]*Item, error) - - // Delete deletes the item with the provided key. - // The error ErrNotFound is returned if the item didn't already exist in - // the cache. - Delete(key string) error - - // Increment atomically increments key by delta. The return value is the - // new value after being incremented or an error. If the value didn't exist - // in memcached the error is ErrNotFound. The value in memcached must be - // an decimal number, or an error will be returned. - // On 64-bit overflow, the new value wraps around. - Increment(key string, delta uint64) (newValue uint64, err error) - - // Decrement atomically decrements key by delta. The return value is the - // new value after being decremented or an error. If the value didn't exist - // in memcached the error is ErrNotFound. The value in memcached must be - // an decimal number, or an error will be returned. On underflow, the new - // value is capped at zero and does not wrap around. - Decrement(key string, delta uint64) (newValue uint64, err error) - - // CompareAndSwap writes the given item that was previously returned by - // Get, if the value was neither modified or evicted between the Get and - // the CompareAndSwap calls. The item's Key should not change between calls - // but all other item fields may differ. ErrCASConflict is returned if the - // value was modified in between the calls. - // ErrNotStored is returned if the value was evicted in between the calls. - CompareAndSwap(item *Item) error - - // Touch updates the expiry for the given key. The seconds parameter is - // either a Unix timestamp or, if seconds is less than 1 month, the number - // of seconds into the future at which time the item will expire. - // ErrNotFound is returned if the key is not in the cache. The key must be - // at most 250 bytes in length. - Touch(key string, seconds int32) (err error) - - // Scan converts value read from the memcache into the following - // common Go types and special types: - // - // *string - // *[]byte - // *interface{} - // - Scan(item *Item, v interface{}) (err error) - - // Add writes the given item, if no value already exists for its key. - // ErrNotStored is returned if that condition is not met. - AddContext(ctx context.Context, item *Item) error - - // Set writes the given item, unconditionally. - SetContext(ctx context.Context, item *Item) error - - // Replace writes the given item, but only if the server *does* already - // hold data for this key. - ReplaceContext(ctx context.Context, item *Item) error - - // Get sends a command to the server for gets data. - GetContext(ctx context.Context, key string) (*Item, error) - - // GetMulti is a batch version of Get. The returned map from keys to items - // may have fewer elements than the input slice, due to memcache cache - // misses. Each key must be at most 250 bytes in length. - // If no error is returned, the returned map will also be non-nil. - GetMultiContext(ctx context.Context, keys []string) (map[string]*Item, error) - - // Delete deletes the item with the provided key. - // The error ErrNotFound is returned if the item didn't already exist in - // the cache. - DeleteContext(ctx context.Context, key string) error - - // Increment atomically increments key by delta. The return value is the - // new value after being incremented or an error. If the value didn't exist - // in memcached the error is ErrNotFound. The value in memcached must be - // an decimal number, or an error will be returned. - // On 64-bit overflow, the new value wraps around. - IncrementContext(ctx context.Context, key string, delta uint64) (newValue uint64, err error) - - // Decrement atomically decrements key by delta. The return value is the - // new value after being decremented or an error. If the value didn't exist - // in memcached the error is ErrNotFound. The value in memcached must be - // an decimal number, or an error will be returned. On underflow, the new - // value is capped at zero and does not wrap around. - DecrementContext(ctx context.Context, key string, delta uint64) (newValue uint64, err error) - - // CompareAndSwap writes the given item that was previously returned by - // Get, if the value was neither modified or evicted between the Get and - // the CompareAndSwap calls. The item's Key should not change between calls - // but all other item fields may differ. ErrCASConflict is returned if the - // value was modified in between the calls. - // ErrNotStored is returned if the value was evicted in between the calls. - CompareAndSwapContext(ctx context.Context, item *Item) error - - // Touch updates the expiry for the given key. The seconds parameter is - // either a Unix timestamp or, if seconds is less than 1 month, the number - // of seconds into the future at which time the item will expire. - // ErrNotFound is returned if the key is not in the cache. The key must be - // at most 250 bytes in length. - TouchContext(ctx context.Context, key string, seconds int32) (err error) -} - -// Config memcache config. -type Config struct { - *pool.Config - - Name string // memcache name, for trace - Proto string - Addr string - DialTimeout xtime.Duration - ReadTimeout xtime.Duration - WriteTimeout xtime.Duration -} - -// Memcache memcache client -type Memcache struct { - pool *Pool -} - -// Reply is the result of Get -type Reply struct { - err error - item *Item - conn Conn - closed bool -} - -// Replies is the result of GetMulti -type Replies struct { - err error - items map[string]*Item - usedItems map[string]struct{} - conn Conn - closed bool -} - -// New get a memcache client -func New(cfg *Config) *Memcache { - return &Memcache{pool: NewPool(cfg)} -} - -// Close close connection pool -func (mc *Memcache) Close() error { - return mc.pool.Close() -} - -// Conn direct get a connection -func (mc *Memcache) Conn(ctx context.Context) Conn { - return mc.pool.Get(ctx) -} - -// Set writes the given item, unconditionally. -func (mc *Memcache) Set(ctx context.Context, item *Item) (err error) { - conn := mc.pool.Get(ctx) - err = conn.SetContext(ctx, item) - conn.Close() - return -} - -// Add writes the given item, if no value already exists for its key. -// ErrNotStored is returned if that condition is not met. -func (mc *Memcache) Add(ctx context.Context, item *Item) (err error) { - conn := mc.pool.Get(ctx) - err = conn.AddContext(ctx, item) - conn.Close() - return -} - -// Replace writes the given item, but only if the server *does* already hold data for this key. -func (mc *Memcache) Replace(ctx context.Context, item *Item) (err error) { - conn := mc.pool.Get(ctx) - err = conn.ReplaceContext(ctx, item) - conn.Close() - return -} - -// CompareAndSwap writes the given item that was previously returned by Get -func (mc *Memcache) CompareAndSwap(ctx context.Context, item *Item) (err error) { - conn := mc.pool.Get(ctx) - err = conn.CompareAndSwapContext(ctx, item) - conn.Close() - return -} - -// Get sends a command to the server for gets data. -func (mc *Memcache) Get(ctx context.Context, key string) *Reply { - conn := mc.pool.Get(ctx) - item, err := conn.GetContext(ctx, key) - if err != nil { - conn.Close() - } - return &Reply{err: err, item: item, conn: conn} -} - -// Item get raw Item -func (r *Reply) Item() *Item { - return r.item -} - -// Scan converts value, read from the memcache -func (r *Reply) Scan(v interface{}) (err error) { - if r.err != nil { - return r.err - } - err = r.conn.Scan(r.item, v) - if !r.closed { - r.conn.Close() - r.closed = true - } - return -} - -// GetMulti is a batch version of Get -func (mc *Memcache) GetMulti(ctx context.Context, keys []string) (*Replies, error) { - conn := mc.pool.Get(ctx) - items, err := conn.GetMultiContext(ctx, keys) - rs := &Replies{err: err, items: items, conn: conn, usedItems: make(map[string]struct{}, len(keys))} - if (err != nil) || (len(items) == 0) { - rs.Close() - } - return rs, err -} - -// Close close rows. -func (rs *Replies) Close() (err error) { - if !rs.closed { - err = rs.conn.Close() - rs.closed = true - } - return -} - -// Item get Item from rows -func (rs *Replies) Item(key string) *Item { - return rs.items[key] -} - -// Scan converts value, read from key in rows -func (rs *Replies) Scan(key string, v interface{}) (err error) { - if rs.err != nil { - return rs.err - } - item, ok := rs.items[key] - if !ok { - rs.Close() - return ErrNotFound - } - rs.usedItems[key] = struct{}{} - err = rs.conn.Scan(item, v) - if (err != nil) || (len(rs.items) == len(rs.usedItems)) { - rs.Close() - } - return -} - -// Keys keys of result -func (rs *Replies) Keys() (keys []string) { - keys = make([]string, 0, len(rs.items)) - for key := range rs.items { - keys = append(keys, key) - } - return -} - -// Touch updates the expiry for the given key. -func (mc *Memcache) Touch(ctx context.Context, key string, timeout int32) (err error) { - conn := mc.pool.Get(ctx) - err = conn.TouchContext(ctx, key, timeout) - conn.Close() - return -} - -// Delete deletes the item with the provided key. -func (mc *Memcache) Delete(ctx context.Context, key string) (err error) { - conn := mc.pool.Get(ctx) - err = conn.DeleteContext(ctx, key) - conn.Close() - return -} - -// Increment atomically increments key by delta. -func (mc *Memcache) Increment(ctx context.Context, key string, delta uint64) (newValue uint64, err error) { - conn := mc.pool.Get(ctx) - newValue, err = conn.IncrementContext(ctx, key, delta) - conn.Close() - return -} - -// Decrement atomically decrements key by delta. -func (mc *Memcache) Decrement(ctx context.Context, key string, delta uint64) (newValue uint64, err error) { - conn := mc.pool.Get(ctx) - newValue, err = conn.DecrementContext(ctx, key, delta) - conn.Close() - return -} diff --git a/pkg/cache/memcache/memcache_test.go b/pkg/cache/memcache/memcache_test.go deleted file mode 100644 index 878841c6a..000000000 --- a/pkg/cache/memcache/memcache_test.go +++ /dev/null @@ -1,300 +0,0 @@ -package memcache - -import ( - "context" - "fmt" - "reflect" - "testing" - "time" -) - -func Test_client_Set(t *testing.T) { - type args struct { - c context.Context - item *Item - } - tests := []struct { - name string - args args - wantErr bool - }{ - {name: "set value", args: args{c: context.Background(), item: &Item{Key: "Test_client_Set", Value: []byte("abc")}}, wantErr: false}, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - if err := testMemcache.Set(tt.args.c, tt.args.item); (err != nil) != tt.wantErr { - t.Errorf("client.Set() error = %v, wantErr %v", err, tt.wantErr) - } - }) - } -} - -func Test_client_Add(t *testing.T) { - type args struct { - c context.Context - item *Item - } - key := fmt.Sprintf("Test_client_Add_%d", time.Now().Unix()) - tests := []struct { - name string - args args - wantErr bool - }{ - {name: "add not exist value", args: args{c: context.Background(), item: &Item{Key: key, Value: []byte("abc")}}, wantErr: false}, - {name: "add exist value", args: args{c: context.Background(), item: &Item{Key: key, Value: []byte("abc")}}, wantErr: true}, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - if err := testMemcache.Add(tt.args.c, tt.args.item); (err != nil) != tt.wantErr { - t.Errorf("client.Add() error = %v, wantErr %v", err, tt.wantErr) - } - }) - } -} - -func Test_client_Replace(t *testing.T) { - key := fmt.Sprintf("Test_client_Replace_%d", time.Now().Unix()) - ekey := "Test_client_Replace_exist" - testMemcache.Set(context.Background(), &Item{Key: ekey, Value: []byte("ok")}) - type args struct { - c context.Context - item *Item - } - tests := []struct { - name string - args args - wantErr bool - }{ - {name: "not exist value", args: args{c: context.Background(), item: &Item{Key: key, Value: []byte("abc")}}, wantErr: true}, - {name: "exist value", args: args{c: context.Background(), item: &Item{Key: ekey, Value: []byte("abc")}}, wantErr: false}, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - if err := testMemcache.Replace(tt.args.c, tt.args.item); (err != nil) != tt.wantErr { - t.Errorf("client.Replace() error = %v, wantErr %v", err, tt.wantErr) - } - }) - } -} - -func Test_client_CompareAndSwap(t *testing.T) { - key := fmt.Sprintf("Test_client_CompareAndSwap_%d", time.Now().Unix()) - ekey := "Test_client_CompareAndSwap_k" - testMemcache.Set(context.Background(), &Item{Key: ekey, Value: []byte("old")}) - cas := testMemcache.Get(context.Background(), ekey).Item().cas - type args struct { - c context.Context - item *Item - } - tests := []struct { - name string - args args - wantErr bool - }{ - {name: "not exist value", args: args{c: context.Background(), item: &Item{Key: key, Value: []byte("abc")}}, wantErr: true}, - {name: "exist value", args: args{c: context.Background(), item: &Item{Key: ekey, cas: cas, Value: []byte("abc")}}, wantErr: false}, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - if err := testMemcache.CompareAndSwap(tt.args.c, tt.args.item); (err != nil) != tt.wantErr { - t.Errorf("client.CompareAndSwap() error = %v, wantErr %v", err, tt.wantErr) - } - }) - } -} - -func Test_client_Get(t *testing.T) { - key := fmt.Sprintf("Test_client_Get_%d", time.Now().Unix()) - ekey := "Test_client_Get_k" - testMemcache.Set(context.Background(), &Item{Key: ekey, Value: []byte("old")}) - type args struct { - c context.Context - key string - } - tests := []struct { - name string - args args - want string - wantErr bool - }{ - {name: "not exist value", args: args{c: context.Background(), key: key}, wantErr: true}, - {name: "exist value", args: args{c: context.Background(), key: ekey}, wantErr: false, want: "old"}, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - var res string - if err := testMemcache.Get(tt.args.c, tt.args.key).Scan(&res); (err != nil) != tt.wantErr || res != tt.want { - t.Errorf("client.Get() = %v, want %v, got err: %v, want err: %v", err, tt.want, err, tt.wantErr) - } - }) - } -} - -func Test_client_Touch(t *testing.T) { - key := fmt.Sprintf("Test_client_Touch_%d", time.Now().Unix()) - ekey := "Test_client_Touch_k" - testMemcache.Set(context.Background(), &Item{Key: ekey, Value: []byte("old")}) - type args struct { - c context.Context - key string - timeout int32 - } - tests := []struct { - name string - args args - wantErr bool - }{ - {name: "not exist value", args: args{c: context.Background(), key: key, timeout: 100000}, wantErr: true}, - {name: "exist value", args: args{c: context.Background(), key: ekey, timeout: 100000}, wantErr: false}, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - if err := testMemcache.Touch(tt.args.c, tt.args.key, tt.args.timeout); (err != nil) != tt.wantErr { - t.Errorf("client.Touch() error = %v, wantErr %v", err, tt.wantErr) - } - }) - } -} - -func Test_client_Delete(t *testing.T) { - key := fmt.Sprintf("Test_client_Delete_%d", time.Now().Unix()) - ekey := "Test_client_Delete_k" - testMemcache.Set(context.Background(), &Item{Key: ekey, Value: []byte("old")}) - type args struct { - c context.Context - key string - } - tests := []struct { - name string - args args - wantErr bool - }{ - {name: "not exist value", args: args{c: context.Background(), key: key}, wantErr: true}, - {name: "exist value", args: args{c: context.Background(), key: ekey}, wantErr: false}, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - if err := testMemcache.Delete(tt.args.c, tt.args.key); (err != nil) != tt.wantErr { - t.Errorf("client.Delete() error = %v, wantErr %v", err, tt.wantErr) - } - }) - } -} - -func Test_client_Increment(t *testing.T) { - key := fmt.Sprintf("Test_client_Increment_%d", time.Now().Unix()) - ekey := "Test_client_Increment_k" - testMemcache.Set(context.Background(), &Item{Key: ekey, Value: []byte("1")}) - type args struct { - c context.Context - key string - delta uint64 - } - tests := []struct { - name string - args args - wantNewValue uint64 - wantErr bool - }{ - {name: "not exist value", args: args{c: context.Background(), key: key, delta: 10}, wantErr: true}, - {name: "exist value", args: args{c: context.Background(), key: ekey, delta: 10}, wantErr: false, wantNewValue: 11}, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - gotNewValue, err := testMemcache.Increment(tt.args.c, tt.args.key, tt.args.delta) - if (err != nil) != tt.wantErr { - t.Errorf("client.Increment() error = %v, wantErr %v", err, tt.wantErr) - return - } - if gotNewValue != tt.wantNewValue { - t.Errorf("client.Increment() = %v, want %v", gotNewValue, tt.wantNewValue) - } - }) - } -} - -func Test_client_Decrement(t *testing.T) { - key := fmt.Sprintf("Test_client_Decrement_%d", time.Now().Unix()) - ekey := "Test_client_Decrement_k" - testMemcache.Set(context.Background(), &Item{Key: ekey, Value: []byte("100")}) - type args struct { - c context.Context - key string - delta uint64 - } - tests := []struct { - name string - args args - wantNewValue uint64 - wantErr bool - }{ - {name: "not exist value", args: args{c: context.Background(), key: key, delta: 10}, wantErr: true}, - {name: "exist value", args: args{c: context.Background(), key: ekey, delta: 10}, wantErr: false, wantNewValue: 90}, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - gotNewValue, err := testMemcache.Decrement(tt.args.c, tt.args.key, tt.args.delta) - if (err != nil) != tt.wantErr { - t.Errorf("client.Decrement() error = %v, wantErr %v", err, tt.wantErr) - return - } - if gotNewValue != tt.wantNewValue { - t.Errorf("client.Decrement() = %v, want %v", gotNewValue, tt.wantNewValue) - } - }) - } -} - -func Test_client_GetMulti(t *testing.T) { - key := fmt.Sprintf("Test_client_GetMulti_%d", time.Now().Unix()) - ekey1 := "Test_client_GetMulti_k1" - ekey2 := "Test_client_GetMulti_k2" - testMemcache.Set(context.Background(), &Item{Key: ekey1, Value: []byte("1")}) - testMemcache.Set(context.Background(), &Item{Key: ekey2, Value: []byte("2")}) - keys := []string{key, ekey1, ekey2} - rows, err := testMemcache.GetMulti(context.Background(), keys) - if err != nil { - t.Errorf("client.GetMulti() error = %v, wantErr %v", err, nil) - } - tests := []struct { - key string - wantNewValue string - wantErr bool - nilItem bool - }{ - {key: ekey1, wantErr: false, wantNewValue: "1", nilItem: false}, - {key: ekey2, wantErr: false, wantNewValue: "2", nilItem: false}, - {key: key, wantErr: true, nilItem: true}, - } - if reflect.DeepEqual(keys, rows.Keys()) { - t.Errorf("got %v, expect: %v", rows.Keys(), keys) - } - for _, tt := range tests { - t.Run(tt.key, func(t *testing.T) { - var gotNewValue string - err = rows.Scan(tt.key, &gotNewValue) - if (err != nil) != tt.wantErr { - t.Errorf("rows.Scan() error = %v, wantErr %v", err, tt.wantErr) - return - } - if gotNewValue != tt.wantNewValue { - t.Errorf("rows.Value() = %v, want %v", gotNewValue, tt.wantNewValue) - } - if (rows.Item(tt.key) == nil) != tt.nilItem { - t.Errorf("rows.Item() = %v, want %v", rows.Item(tt.key) == nil, tt.nilItem) - } - }) - } - err = rows.Close() - if err != nil { - t.Errorf("client.Replies.Close() error = %v, wantErr %v", err, nil) - } -} - -func Test_client_Conn(t *testing.T) { - conn := testMemcache.Conn(context.Background()) - defer conn.Close() - if conn == nil { - t.Errorf("expect get conn, get nil") - } -} diff --git a/pkg/cache/memcache/metrics.go b/pkg/cache/memcache/metrics.go deleted file mode 100644 index 0eeb60573..000000000 --- a/pkg/cache/memcache/metrics.go +++ /dev/null @@ -1,51 +0,0 @@ -package memcache - -import "github.com/go-kratos/kratos/pkg/stat/metric" - -const namespace = "memcache_client" - -var ( - _metricReqDur = metric.NewHistogramVec(&metric.HistogramVecOpts{ - Namespace: namespace, - Subsystem: "requests", - Name: "duration_ms", - Help: "memcache client requests duration(ms).", - Labels: []string{"name", "addr", "command"}, - Buckets: []float64{5, 10, 25, 50, 100, 250, 500, 1000, 2500}, - }) - _metricReqErr = metric.NewCounterVec(&metric.CounterVecOpts{ - Namespace: namespace, - Subsystem: "requests", - Name: "error_total", - Help: "memcache client requests error count.", - Labels: []string{"name", "addr", "command", "error"}, - }) - _metricConnTotal = metric.NewCounterVec(&metric.CounterVecOpts{ - Namespace: namespace, - Subsystem: "connections", - Name: "total", - Help: "memcache client connections total count.", - Labels: []string{"name", "addr", "state"}, - }) - _metricConnCurrent = metric.NewGaugeVec(&metric.GaugeVecOpts{ - Namespace: namespace, - Subsystem: "connections", - Name: "current", - Help: "memcache client connections current.", - Labels: []string{"name", "addr", "state"}, - }) - _metricHits = metric.NewCounterVec(&metric.CounterVecOpts{ - Namespace: namespace, - Subsystem: "", - Name: "hits_total", - Help: "memcache client hits total.", - Labels: []string{"name", "addr"}, - }) - _metricMisses = metric.NewCounterVec(&metric.CounterVecOpts{ - Namespace: namespace, - Subsystem: "", - Name: "misses_total", - Help: "memcache client misses total.", - Labels: []string{"name", "addr"}, - }) -) diff --git a/pkg/cache/memcache/pool_conn.go b/pkg/cache/memcache/pool_conn.go deleted file mode 100644 index 6de7a31cd..000000000 --- a/pkg/cache/memcache/pool_conn.go +++ /dev/null @@ -1,203 +0,0 @@ -package memcache - -import ( - "context" - "fmt" - "io" - "time" - - "github.com/go-kratos/kratos/pkg/container/pool" -) - -// Pool memcache connection pool struct. -// Deprecated: Use Memcache instead -type Pool struct { - p pool.Pool - c *Config -} - -// NewPool new a memcache conn pool. -// Deprecated: Use New instead -func NewPool(cfg *Config) (p *Pool) { - if cfg.DialTimeout <= 0 || cfg.ReadTimeout <= 0 || cfg.WriteTimeout <= 0 { - panic("must config memcache timeout") - } - p1 := pool.NewList(cfg.Config) - cnop := DialConnectTimeout(time.Duration(cfg.DialTimeout)) - rdop := DialReadTimeout(time.Duration(cfg.ReadTimeout)) - wrop := DialWriteTimeout(time.Duration(cfg.WriteTimeout)) - p1.New = func(ctx context.Context) (io.Closer, error) { - conn, err := Dial(cfg.Proto, cfg.Addr, cnop, rdop, wrop) - return newTraceConn(conn, fmt.Sprintf("%s://%s", cfg.Proto, cfg.Addr)), err - } - p = &Pool{p: p1, c: cfg} - return -} - -// Get gets a connection. The application must close the returned connection. -// This method always returns a valid connection so that applications can defer -// error handling to the first use of the connection. If there is an error -// getting an underlying connection, then the connection Err, Do, Send, Flush -// and Receive methods return that error. -func (p *Pool) Get(ctx context.Context) Conn { - c, err := p.p.Get(ctx) - if err != nil { - return errConn{err} - } - c1, _ := c.(Conn) - return &poolConn{p: p, c: c1, ctx: ctx} -} - -// Close release the resources used by the pool. -func (p *Pool) Close() error { - return p.p.Close() -} - -type poolConn struct { - c Conn - p *Pool - ctx context.Context -} - -func (pc *poolConn) pstat(key string, t time.Time, err error) { - _metricReqDur.Observe(int64(time.Since(t)/time.Millisecond), pc.p.c.Name, pc.p.c.Addr, key) - if err != nil { - if msg := pc.formatErr(err); msg != "" { - _metricReqErr.Inc(pc.p.c.Name, pc.p.c.Addr, key, msg) - } - return - } - _metricHits.Inc(pc.p.c.Name, pc.p.c.Addr) -} - -func (pc *poolConn) Close() error { - c := pc.c - if _, ok := c.(errConn); ok { - return nil - } - pc.c = errConn{ErrConnClosed} - pc.p.p.Put(context.Background(), c, c.Err() != nil) - return nil -} - -func (pc *poolConn) Err() error { - return pc.c.Err() -} - -func (pc *poolConn) Set(item *Item) (err error) { - return pc.SetContext(pc.ctx, item) -} - -func (pc *poolConn) Add(item *Item) (err error) { - return pc.AddContext(pc.ctx, item) -} - -func (pc *poolConn) Replace(item *Item) (err error) { - return pc.ReplaceContext(pc.ctx, item) -} - -func (pc *poolConn) CompareAndSwap(item *Item) (err error) { - return pc.CompareAndSwapContext(pc.ctx, item) -} - -func (pc *poolConn) Get(key string) (r *Item, err error) { - return pc.GetContext(pc.ctx, key) -} - -func (pc *poolConn) GetMulti(keys []string) (res map[string]*Item, err error) { - return pc.GetMultiContext(pc.ctx, keys) -} - -func (pc *poolConn) Touch(key string, timeout int32) (err error) { - return pc.TouchContext(pc.ctx, key, timeout) -} - -func (pc *poolConn) Scan(item *Item, v interface{}) error { - return pc.c.Scan(item, v) -} - -func (pc *poolConn) Delete(key string) (err error) { - return pc.DeleteContext(pc.ctx, key) -} - -func (pc *poolConn) Increment(key string, delta uint64) (newValue uint64, err error) { - return pc.IncrementContext(pc.ctx, key, delta) -} - -func (pc *poolConn) Decrement(key string, delta uint64) (newValue uint64, err error) { - return pc.DecrementContext(pc.ctx, key, delta) -} - -func (pc *poolConn) AddContext(ctx context.Context, item *Item) error { - now := time.Now() - err := pc.c.AddContext(ctx, item) - pc.pstat("add", now, err) - return err -} - -func (pc *poolConn) SetContext(ctx context.Context, item *Item) error { - now := time.Now() - err := pc.c.SetContext(ctx, item) - pc.pstat("set", now, err) - return err -} - -func (pc *poolConn) ReplaceContext(ctx context.Context, item *Item) error { - now := time.Now() - err := pc.c.ReplaceContext(ctx, item) - pc.pstat("replace", now, err) - return err -} - -func (pc *poolConn) GetContext(ctx context.Context, key string) (*Item, error) { - now := time.Now() - item, err := pc.c.Get(key) - pc.pstat("get", now, err) - return item, err -} - -func (pc *poolConn) GetMultiContext(ctx context.Context, keys []string) (map[string]*Item, error) { - // if keys is empty slice returns empty map direct - if len(keys) == 0 { - return make(map[string]*Item), nil - } - now := time.Now() - items, err := pc.c.GetMulti(keys) - pc.pstat("gets", now, err) - return items, err -} - -func (pc *poolConn) DeleteContext(ctx context.Context, key string) error { - now := time.Now() - err := pc.c.Delete(key) - pc.pstat("delete", now, err) - return err -} - -func (pc *poolConn) IncrementContext(ctx context.Context, key string, delta uint64) (uint64, error) { - now := time.Now() - newValue, err := pc.c.IncrementContext(ctx, key, delta) - pc.pstat("increment", now, err) - return newValue, err -} - -func (pc *poolConn) DecrementContext(ctx context.Context, key string, delta uint64) (uint64, error) { - now := time.Now() - newValue, err := pc.c.DecrementContext(ctx, key, delta) - pc.pstat("decrement", now, err) - return newValue, err -} - -func (pc *poolConn) CompareAndSwapContext(ctx context.Context, item *Item) error { - now := time.Now() - err := pc.c.CompareAndSwap(item) - pc.pstat("cas", now, err) - return err -} - -func (pc *poolConn) TouchContext(ctx context.Context, key string, seconds int32) error { - now := time.Now() - err := pc.c.Touch(key, seconds) - pc.pstat("touch", now, err) - return err -} diff --git a/pkg/cache/memcache/pool_conn_test.go b/pkg/cache/memcache/pool_conn_test.go deleted file mode 100644 index 35505945f..000000000 --- a/pkg/cache/memcache/pool_conn_test.go +++ /dev/null @@ -1,543 +0,0 @@ -package memcache - -import ( - "bytes" - "context" - "reflect" - "testing" - "time" - - "github.com/go-kratos/kratos/pkg/container/pool" - xtime "github.com/go-kratos/kratos/pkg/time" -) - -var itempool = &Item{ - Key: "testpool", - Value: []byte("testpool"), - Flags: 0, - Expiration: 60, - cas: 0, -} -var itempool2 = &Item{ - Key: "test_count", - Value: []byte("0"), - Flags: 0, - Expiration: 1000, - cas: 0, -} - -type testObject struct { - Mid int64 - Value []byte -} - -var largeValue = &Item{ - Key: "large_value", - Flags: FlagGOB | FlagGzip, - Expiration: 1000, - cas: 0, -} - -var largeValueBoundary = &Item{ - Key: "large_value", - Flags: FlagGOB | FlagGzip, - Expiration: 1000, - cas: 0, -} - -func TestPoolSet(t *testing.T) { - conn := testPool.Get(context.Background()) - defer conn.Close() - // set - if err := conn.Set(itempool); err != nil { - t.Errorf("memcache: set error(%v)", err) - } else { - t.Logf("memcache: set value: %s", itempool.Value) - } - if err := conn.Close(); err != nil { - t.Errorf("memcache: close error(%v)", err) - } -} - -func TestPoolGet(t *testing.T) { - key := "testpool" - conn := testPool.Get(context.Background()) - defer conn.Close() - // get - if res, err := conn.Get(key); err != nil { - t.Errorf("memcache: get error(%v)", err) - } else { - t.Logf("memcache: get value: %s", res.Value) - } - if _, err := conn.Get("not_found"); err != ErrNotFound { - t.Errorf("memcache: expceted err is not found but got: %v", err) - } - if err := conn.Close(); err != nil { - t.Errorf("memcache: close error(%v)", err) - } -} - -func TestPoolGetMulti(t *testing.T) { - conn := testPool.Get(context.Background()) - defer conn.Close() - s := []string{"testpool", "test1"} - // get - if res, err := conn.GetMulti(s); err != nil { - t.Errorf("memcache: gets error(%v)", err) - } else { - t.Logf("memcache: gets value: %d", len(res)) - } - if err := conn.Close(); err != nil { - t.Errorf("memcache: close error(%v)", err) - } -} - -func TestPoolTouch(t *testing.T) { - key := "testpool" - conn := testPool.Get(context.Background()) - defer conn.Close() - // touch - if err := conn.Touch(key, 10); err != nil { - t.Errorf("memcache: touch error(%v)", err) - } - if err := conn.Close(); err != nil { - t.Errorf("memcache: close error(%v)", err) - } -} - -func TestPoolIncrement(t *testing.T) { - key := "test_count" - conn := testPool.Get(context.Background()) - defer conn.Close() - // set - if err := conn.Set(itempool2); err != nil { - t.Errorf("memcache: set error(%v)", err) - } else { - t.Logf("memcache: set value: 0") - } - // incr - if res, err := conn.Increment(key, 1); err != nil { - t.Errorf("memcache: incr error(%v)", err) - } else { - t.Logf("memcache: incr n: %d", res) - if res != 1 { - t.Errorf("memcache: expected res=1 but got %d", res) - } - } - // decr - if res, err := conn.Decrement(key, 1); err != nil { - t.Errorf("memcache: decr error(%v)", err) - } else { - t.Logf("memcache: decr n: %d", res) - if res != 0 { - t.Errorf("memcache: expected res=0 but got %d", res) - } - } - if err := conn.Close(); err != nil { - t.Errorf("memcache: close error(%v)", err) - } -} - -func TestPoolErr(t *testing.T) { - conn := testPool.Get(context.Background()) - defer conn.Close() - if err := conn.Close(); err != nil { - t.Errorf("memcache: close error(%v)", err) - } - if err := conn.Err(); err == nil { - t.Errorf("memcache: err not nil") - } else { - t.Logf("memcache: err: %v", err) - } -} - -func TestPoolCompareAndSwap(t *testing.T) { - conn := testPool.Get(context.Background()) - defer conn.Close() - key := "testpool" - //cas - if r, err := conn.Get(key); err != nil { - t.Errorf("conn.Get() error(%v)", err) - } else { - r.Value = []byte("shit") - if err := conn.CompareAndSwap(r); err != nil { - t.Errorf("conn.Get() error(%v)", err) - } - r, _ := conn.Get("testpool") - if r.Key != "testpool" || !bytes.Equal(r.Value, []byte("shit")) || r.Flags != 0 { - t.Error("conn.Get() error, value") - } - if err := conn.Close(); err != nil { - t.Errorf("memcache: close error(%v)", err) - } - } -} - -func TestPoolDel(t *testing.T) { - key := "testpool" - conn := testPool.Get(context.Background()) - defer conn.Close() - // delete - if err := conn.Delete(key); err != nil { - t.Errorf("memcache: delete error(%v)", err) - } else { - t.Logf("memcache: delete key: %s", key) - } - if err := conn.Close(); err != nil { - t.Errorf("memcache: close error(%v)", err) - } -} - -func BenchmarkMemcache(b *testing.B) { - c := &Config{ - Name: "test", - Proto: "tcp", - Addr: testMemcacheAddr, - DialTimeout: xtime.Duration(time.Second), - ReadTimeout: xtime.Duration(time.Second), - WriteTimeout: xtime.Duration(time.Second), - } - c.Config = &pool.Config{ - Active: 10, - Idle: 5, - IdleTimeout: xtime.Duration(90 * time.Second), - } - testPool = NewPool(c) - b.ResetTimer() - b.RunParallel(func(pb *testing.PB) { - for pb.Next() { - conn := testPool.Get(context.Background()) - if err := conn.Close(); err != nil { - b.Errorf("memcache: close error(%v)", err) - } - } - }) - if err := testPool.Close(); err != nil { - b.Errorf("memcache: close error(%v)", err) - } -} - -func TestPoolSetLargeValue(t *testing.T) { - var b bytes.Buffer - for i := 0; i < 4000000; i++ { - b.WriteByte(1) - } - obj := &testObject{} - obj.Mid = 1000 - obj.Value = b.Bytes() - largeValue.Object = obj - conn := testPool.Get(context.Background()) - defer conn.Close() - // set - if err := conn.Set(largeValue); err != nil { - t.Errorf("memcache: set error(%v)", err) - } - if err := conn.Close(); err != nil { - t.Errorf("memcache: close error(%v)", err) - } -} - -func TestPoolGetLargeValue(t *testing.T) { - key := largeValue.Key - conn := testPool.Get(context.Background()) - defer conn.Close() - // get - var err error - if _, err = conn.Get(key); err != nil { - t.Errorf("memcache: large get error(%+v)", err) - } -} - -func TestPoolGetMultiLargeValue(t *testing.T) { - conn := testPool.Get(context.Background()) - defer conn.Close() - s := []string{largeValue.Key, largeValue.Key} - // get - if res, err := conn.GetMulti(s); err != nil { - t.Errorf("memcache: gets error(%v)", err) - } else { - t.Logf("memcache: gets value: %d", len(res)) - } - if err := conn.Close(); err != nil { - t.Errorf("memcache: close error(%v)", err) - } -} - -func TestPoolSetLargeValueBoundary(t *testing.T) { - var b bytes.Buffer - for i := 0; i < _largeValue; i++ { - b.WriteByte(1) - } - obj := &testObject{} - obj.Mid = 1000 - obj.Value = b.Bytes() - largeValueBoundary.Object = obj - conn := testPool.Get(context.Background()) - defer conn.Close() - // set - if err := conn.Set(largeValueBoundary); err != nil { - t.Errorf("memcache: set error(%v)", err) - } - if err := conn.Close(); err != nil { - t.Errorf("memcache: close error(%v)", err) - } -} - -func TestPoolGetLargeValueBoundary(t *testing.T) { - key := largeValueBoundary.Key - conn := testPool.Get(context.Background()) - defer conn.Close() - // get - var err error - if _, err = conn.Get(key); err != nil { - t.Errorf("memcache: large get error(%v)", err) - } -} - -func TestPoolAdd(t *testing.T) { - var ( - key = "test_add" - item = &Item{ - Key: key, - Value: []byte("0"), - Flags: 0, - Expiration: 60, - cas: 0, - } - conn = testPool.Get(context.Background()) - ) - defer conn.Close() - conn.Delete(key) - if err := conn.Add(item); err != nil { - t.Errorf("memcache: add error(%v)", err) - } - if err := conn.Add(item); err != ErrNotStored { - t.Errorf("memcache: add error(%v)", err) - } -} - -func TestNewPool(t *testing.T) { - type args struct { - cfg *Config - } - tests := []struct { - name string - args args - wantErr error - wantPanic bool - }{ - { - "NewPoolIllegalDialTimeout", - args{ - &Config{ - Name: "test_illegal_dial_timeout", - Proto: "tcp", - Addr: testMemcacheAddr, - DialTimeout: xtime.Duration(-time.Second), - ReadTimeout: xtime.Duration(time.Second), - WriteTimeout: xtime.Duration(time.Second), - }, - }, - nil, - true, - }, - { - "NewPoolIllegalReadTimeout", - args{ - &Config{ - Name: "test_illegal_read_timeout", - Proto: "tcp", - Addr: testMemcacheAddr, - DialTimeout: xtime.Duration(time.Second), - ReadTimeout: xtime.Duration(-time.Second), - WriteTimeout: xtime.Duration(time.Second), - }, - }, - nil, - true, - }, - { - "NewPoolIllegalWriteTimeout", - args{ - &Config{ - Name: "test_illegal_write_timeout", - Proto: "tcp", - Addr: testMemcacheAddr, - DialTimeout: xtime.Duration(time.Second), - ReadTimeout: xtime.Duration(time.Second), - WriteTimeout: xtime.Duration(-time.Second), - }, - }, - nil, - true, - }, - { - "NewPool", - args{ - &Config{ - Name: "test_new", - Proto: "tcp", - Addr: testMemcacheAddr, - DialTimeout: xtime.Duration(time.Second), - ReadTimeout: xtime.Duration(time.Second), - WriteTimeout: xtime.Duration(time.Second), - }, - }, - nil, - true, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - defer func() { - r := recover() - if (r != nil) != tt.wantPanic { - t.Errorf("wantPanic recover = %v, wantPanic = %v", r, tt.wantPanic) - } - }() - - if gotP := NewPool(tt.args.cfg); gotP == nil { - t.Error("NewPool() failed, got nil") - } - }) - } -} - -func TestPool_Get(t *testing.T) { - type args struct { - ctx context.Context - } - tests := []struct { - name string - p *Pool - args args - wantErr bool - n int - }{ - { - "Get", - NewPool(&Config{ - Config: &pool.Config{ - Active: 3, - Idle: 2, - }, - Name: "test_get", - Proto: "tcp", - Addr: testMemcacheAddr, - DialTimeout: xtime.Duration(time.Second), - ReadTimeout: xtime.Duration(time.Second), - WriteTimeout: xtime.Duration(time.Second), - }), - args{context.TODO()}, - false, - 3, - }, - { - "GetExceededPoolSize", - NewPool(&Config{ - Config: &pool.Config{ - Active: 3, - Idle: 2, - }, - Name: "test_get_out", - Proto: "tcp", - Addr: testMemcacheAddr, - DialTimeout: xtime.Duration(time.Second), - ReadTimeout: xtime.Duration(time.Second), - WriteTimeout: xtime.Duration(time.Second), - }), - args{context.TODO()}, - true, - 6, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - for i := 1; i <= tt.n; i++ { - got := tt.p.Get(tt.args.ctx) - if reflect.TypeOf(got) == reflect.TypeOf(errConn{}) { - if !tt.wantErr { - t.Errorf("got errConn, export Conn") - } - return - } else { - if tt.wantErr { - if i > tt.p.c.Active { - t.Errorf("got Conn, export errConn") - } - } - } - } - }) - } -} - -func TestPool_Close(t *testing.T) { - type args struct { - ctx context.Context - } - tests := []struct { - name string - p *Pool - args args - wantErr bool - g int - c int - }{ - { - "Close", - NewPool(&Config{ - Config: &pool.Config{ - Active: 1, - Idle: 1, - }, - Name: "test_get", - Proto: "tcp", - Addr: testMemcacheAddr, - DialTimeout: xtime.Duration(time.Second), - ReadTimeout: xtime.Duration(time.Second), - WriteTimeout: xtime.Duration(time.Second), - }), - args{context.TODO()}, - false, - 3, - 3, - }, - { - "CloseExceededPoolSize", - NewPool(&Config{ - Config: &pool.Config{ - Active: 1, - Idle: 1, - }, - Name: "test_get_out", - Proto: "tcp", - Addr: testMemcacheAddr, - DialTimeout: xtime.Duration(time.Second), - ReadTimeout: xtime.Duration(time.Second), - WriteTimeout: xtime.Duration(time.Second), - }), - args{context.TODO()}, - true, - 5, - 3, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - for i := 1; i <= tt.g; i++ { - got := tt.p.Get(tt.args.ctx) - if err := got.Close(); err != nil { - if !tt.wantErr { - t.Error(err) - } - } - if i <= tt.c { - if err := got.Close(); err != nil { - t.Error(err) - } - } - } - }) - } -} diff --git a/pkg/cache/memcache/test/docker-compose.yaml b/pkg/cache/memcache/test/docker-compose.yaml deleted file mode 100755 index ace3ebedf..000000000 --- a/pkg/cache/memcache/test/docker-compose.yaml +++ /dev/null @@ -1,9 +0,0 @@ -version: "3.7" - -services: - mc: - image: memcached:1 - ports: - - 11211:11211 - - diff --git a/pkg/cache/memcache/test/test.pb.go b/pkg/cache/memcache/test/test.pb.go deleted file mode 100644 index 1dc41aa00..000000000 --- a/pkg/cache/memcache/test/test.pb.go +++ /dev/null @@ -1,375 +0,0 @@ -// Code generated by protoc-gen-gogo. DO NOT EDIT. -// source: test.proto - -/* - Package proto is a generated protocol buffer package. - - It is generated from these files: - test.proto - - It has these top-level messages: - TestItem -*/ -package proto - -import proto1 "github.com/golang/protobuf/proto" -import fmt "fmt" -import math "math" - -import io "io" - -// Reference imports to suppress errors if they are not otherwise used. -var _ = proto1.Marshal -var _ = fmt.Errorf -var _ = math.Inf - -// This is a compile-time assertion to ensure that this generated file -// is compatible with the proto package it is being compiled against. -// A compilation error at this line likely means your copy of the -// proto package needs to be updated. -const _ = proto1.ProtoPackageIsVersion2 // please upgrade the proto package - -type FOO int32 - -const ( - FOO_X FOO = 0 -) - -var FOO_name = map[int32]string{ - 0: "X", -} -var FOO_value = map[string]int32{ - "X": 0, -} - -func (x FOO) String() string { - return proto1.EnumName(FOO_name, int32(x)) -} -func (FOO) EnumDescriptor() ([]byte, []int) { return fileDescriptorTest, []int{0} } - -type TestItem struct { - Name string `protobuf:"bytes,1,opt,name=Name,proto3" json:"Name,omitempty"` - Age int32 `protobuf:"varint,2,opt,name=Age,proto3" json:"Age,omitempty"` -} - -func (m *TestItem) Reset() { *m = TestItem{} } -func (m *TestItem) String() string { return proto1.CompactTextString(m) } -func (*TestItem) ProtoMessage() {} -func (*TestItem) Descriptor() ([]byte, []int) { return fileDescriptorTest, []int{0} } - -func (m *TestItem) GetName() string { - if m != nil { - return m.Name - } - return "" -} - -func (m *TestItem) GetAge() int32 { - if m != nil { - return m.Age - } - return 0 -} - -func init() { - proto1.RegisterType((*TestItem)(nil), "proto.TestItem") - proto1.RegisterEnum("proto.FOO", FOO_name, FOO_value) -} -func (m *TestItem) Marshal() (dAtA []byte, err error) { - size := m.Size() - dAtA = make([]byte, size) - n, err := m.MarshalTo(dAtA) - if err != nil { - return nil, err - } - return dAtA[:n], nil -} - -func (m *TestItem) MarshalTo(dAtA []byte) (int, error) { - var i int - _ = i - var l int - _ = l - if len(m.Name) > 0 { - dAtA[i] = 0xa - i++ - i = encodeVarintTest(dAtA, i, uint64(len(m.Name))) - i += copy(dAtA[i:], m.Name) - } - if m.Age != 0 { - dAtA[i] = 0x10 - i++ - i = encodeVarintTest(dAtA, i, uint64(m.Age)) - } - return i, nil -} - -func encodeFixed64Test(dAtA []byte, offset int, v uint64) int { - dAtA[offset] = uint8(v) - dAtA[offset+1] = uint8(v >> 8) - dAtA[offset+2] = uint8(v >> 16) - dAtA[offset+3] = uint8(v >> 24) - dAtA[offset+4] = uint8(v >> 32) - dAtA[offset+5] = uint8(v >> 40) - dAtA[offset+6] = uint8(v >> 48) - dAtA[offset+7] = uint8(v >> 56) - return offset + 8 -} -func encodeFixed32Test(dAtA []byte, offset int, v uint32) int { - dAtA[offset] = uint8(v) - dAtA[offset+1] = uint8(v >> 8) - dAtA[offset+2] = uint8(v >> 16) - dAtA[offset+3] = uint8(v >> 24) - return offset + 4 -} -func encodeVarintTest(dAtA []byte, offset int, v uint64) int { - for v >= 1<<7 { - dAtA[offset] = uint8(v&0x7f | 0x80) - v >>= 7 - offset++ - } - dAtA[offset] = uint8(v) - return offset + 1 -} -func (m *TestItem) Size() (n int) { - var l int - _ = l - l = len(m.Name) - if l > 0 { - n += 1 + l + sovTest(uint64(l)) - } - if m.Age != 0 { - n += 1 + sovTest(uint64(m.Age)) - } - return n -} - -func sovTest(x uint64) (n int) { - for { - n++ - x >>= 7 - if x == 0 { - break - } - } - return n -} -func sozTest(x uint64) (n int) { - return sovTest(uint64((x << 1) ^ uint64((int64(x) >> 63)))) -} -func (m *TestItem) Unmarshal(dAtA []byte) error { - l := len(dAtA) - iNdEx := 0 - for iNdEx < l { - preIndex := iNdEx - var wire uint64 - for shift := uint(0); ; shift += 7 { - if shift >= 64 { - return ErrIntOverflowTest - } - if iNdEx >= l { - return io.ErrUnexpectedEOF - } - b := dAtA[iNdEx] - iNdEx++ - wire |= (uint64(b) & 0x7F) << shift - if b < 0x80 { - break - } - } - fieldNum := int32(wire >> 3) - wireType := int(wire & 0x7) - if wireType == 4 { - return fmt.Errorf("proto: TestItem: wiretype end group for non-group") - } - if fieldNum <= 0 { - return fmt.Errorf("proto: TestItem: illegal tag %d (wire type %d)", fieldNum, wire) - } - switch fieldNum { - case 1: - if wireType != 2 { - return fmt.Errorf("proto: wrong wireType = %d for field Name", wireType) - } - var stringLen uint64 - for shift := uint(0); ; shift += 7 { - if shift >= 64 { - return ErrIntOverflowTest - } - if iNdEx >= l { - return io.ErrUnexpectedEOF - } - b := dAtA[iNdEx] - iNdEx++ - stringLen |= (uint64(b) & 0x7F) << shift - if b < 0x80 { - break - } - } - intStringLen := int(stringLen) - if intStringLen < 0 { - return ErrInvalidLengthTest - } - postIndex := iNdEx + intStringLen - if postIndex > l { - return io.ErrUnexpectedEOF - } - m.Name = string(dAtA[iNdEx:postIndex]) - iNdEx = postIndex - case 2: - if wireType != 0 { - return fmt.Errorf("proto: wrong wireType = %d for field Age", wireType) - } - m.Age = 0 - for shift := uint(0); ; shift += 7 { - if shift >= 64 { - return ErrIntOverflowTest - } - if iNdEx >= l { - return io.ErrUnexpectedEOF - } - b := dAtA[iNdEx] - iNdEx++ - m.Age |= (int32(b) & 0x7F) << shift - if b < 0x80 { - break - } - } - default: - iNdEx = preIndex - skippy, err := skipTest(dAtA[iNdEx:]) - if err != nil { - return err - } - if skippy < 0 { - return ErrInvalidLengthTest - } - if (iNdEx + skippy) > l { - return io.ErrUnexpectedEOF - } - iNdEx += skippy - } - } - - if iNdEx > l { - return io.ErrUnexpectedEOF - } - return nil -} -func skipTest(dAtA []byte) (n int, err error) { - l := len(dAtA) - iNdEx := 0 - for iNdEx < l { - var wire uint64 - for shift := uint(0); ; shift += 7 { - if shift >= 64 { - return 0, ErrIntOverflowTest - } - if iNdEx >= l { - return 0, io.ErrUnexpectedEOF - } - b := dAtA[iNdEx] - iNdEx++ - wire |= (uint64(b) & 0x7F) << shift - if b < 0x80 { - break - } - } - wireType := int(wire & 0x7) - switch wireType { - case 0: - for shift := uint(0); ; shift += 7 { - if shift >= 64 { - return 0, ErrIntOverflowTest - } - if iNdEx >= l { - return 0, io.ErrUnexpectedEOF - } - iNdEx++ - if dAtA[iNdEx-1] < 0x80 { - break - } - } - return iNdEx, nil - case 1: - iNdEx += 8 - return iNdEx, nil - case 2: - var length int - for shift := uint(0); ; shift += 7 { - if shift >= 64 { - return 0, ErrIntOverflowTest - } - if iNdEx >= l { - return 0, io.ErrUnexpectedEOF - } - b := dAtA[iNdEx] - iNdEx++ - length |= (int(b) & 0x7F) << shift - if b < 0x80 { - break - } - } - iNdEx += length - if length < 0 { - return 0, ErrInvalidLengthTest - } - return iNdEx, nil - case 3: - for { - var innerWire uint64 - var start int = iNdEx - for shift := uint(0); ; shift += 7 { - if shift >= 64 { - return 0, ErrIntOverflowTest - } - if iNdEx >= l { - return 0, io.ErrUnexpectedEOF - } - b := dAtA[iNdEx] - iNdEx++ - innerWire |= (uint64(b) & 0x7F) << shift - if b < 0x80 { - break - } - } - innerWireType := int(innerWire & 0x7) - if innerWireType == 4 { - break - } - next, err := skipTest(dAtA[start:]) - if err != nil { - return 0, err - } - iNdEx = start + next - } - return iNdEx, nil - case 4: - return iNdEx, nil - case 5: - iNdEx += 4 - return iNdEx, nil - default: - return 0, fmt.Errorf("proto: illegal wireType %d", wireType) - } - } - panic("unreachable") -} - -var ( - ErrInvalidLengthTest = fmt.Errorf("proto: negative length found during unmarshaling") - ErrIntOverflowTest = fmt.Errorf("proto: integer overflow") -) - -func init() { proto1.RegisterFile("test.proto", fileDescriptorTest) } - -var fileDescriptorTest = []byte{ - // 122 bytes of a gzipped FileDescriptorProto - 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xe2, 0xe2, 0x2a, 0x49, 0x2d, 0x2e, - 0xd1, 0x2b, 0x28, 0xca, 0x2f, 0xc9, 0x17, 0x62, 0x05, 0x53, 0x4a, 0x06, 0x5c, 0x1c, 0x21, 0xa9, - 0xc5, 0x25, 0x9e, 0x25, 0xa9, 0xb9, 0x42, 0x42, 0x5c, 0x2c, 0x7e, 0x89, 0xb9, 0xa9, 0x12, 0x8c, - 0x0a, 0x8c, 0x1a, 0x9c, 0x41, 0x60, 0xb6, 0x90, 0x00, 0x17, 0xb3, 0x63, 0x7a, 0xaa, 0x04, 0x93, - 0x02, 0xa3, 0x06, 0x6b, 0x10, 0x88, 0xa9, 0xc5, 0xc3, 0xc5, 0xec, 0xe6, 0xef, 0x2f, 0xc4, 0xca, - 0xc5, 0x18, 0x21, 0xc0, 0xe0, 0x24, 0x70, 0xe2, 0x91, 0x1c, 0xe3, 0x85, 0x47, 0x72, 0x8c, 0x0f, - 0x1e, 0xc9, 0x31, 0xce, 0x78, 0x2c, 0xc7, 0x90, 0xc4, 0x06, 0x36, 0xd8, 0x18, 0x10, 0x00, 0x00, - 0xff, 0xff, 0x16, 0x80, 0x60, 0x15, 0x6d, 0x00, 0x00, 0x00, -} diff --git a/pkg/cache/memcache/test/test.proto b/pkg/cache/memcache/test/test.proto deleted file mode 100644 index adad15bea..000000000 --- a/pkg/cache/memcache/test/test.proto +++ /dev/null @@ -1,12 +0,0 @@ -syntax = "proto3"; -package proto; - -enum FOO -{ - X = 0; -}; - -message TestItem{ - string Name = 1; - int32 Age = 2; -} \ No newline at end of file diff --git a/pkg/cache/memcache/trace_conn.go b/pkg/cache/memcache/trace_conn.go deleted file mode 100644 index 776a2488c..000000000 --- a/pkg/cache/memcache/trace_conn.go +++ /dev/null @@ -1,103 +0,0 @@ -package memcache - -import ( - "context" - "strconv" - "strings" - "time" - - "github.com/go-kratos/kratos/pkg/log" - "github.com/go-kratos/kratos/pkg/net/trace" -) - -const ( - _slowLogDuration = time.Millisecond * 250 -) - -func newTraceConn(conn Conn, address string) Conn { - tags := []trace.Tag{ - trace.String(trace.TagSpanKind, "client"), - trace.String(trace.TagComponent, "cache/memcache"), - trace.String(trace.TagPeerService, "memcache"), - trace.String(trace.TagPeerAddress, address), - } - return &traceConn{Conn: conn, tags: tags} -} - -type traceConn struct { - Conn - tags []trace.Tag -} - -func (t *traceConn) setTrace(ctx context.Context, action, statement string) func(error) error { - now := time.Now() - parent, ok := trace.FromContext(ctx) - if !ok { - return func(err error) error { return err } - } - span := parent.Fork("", "Memcache:"+action) - span.SetTag(t.tags...) - span.SetTag(trace.String(trace.TagDBStatement, action+" "+statement)) - return func(err error) error { - span.Finish(&err) - t := time.Since(now) - if t > _slowLogDuration { - log.Warn("memcache slow log action: %s key: %s time: %v", action, statement, t) - } - return err - } -} - -func (t *traceConn) AddContext(ctx context.Context, item *Item) error { - finishFn := t.setTrace(ctx, "Add", item.Key) - return finishFn(t.Conn.Add(item)) -} - -func (t *traceConn) SetContext(ctx context.Context, item *Item) error { - finishFn := t.setTrace(ctx, "Set", item.Key) - return finishFn(t.Conn.Set(item)) -} - -func (t *traceConn) ReplaceContext(ctx context.Context, item *Item) error { - finishFn := t.setTrace(ctx, "Replace", item.Key) - return finishFn(t.Conn.Replace(item)) -} - -func (t *traceConn) GetContext(ctx context.Context, key string) (*Item, error) { - finishFn := t.setTrace(ctx, "Get", key) - item, err := t.Conn.Get(key) - return item, finishFn(err) -} - -func (t *traceConn) GetMultiContext(ctx context.Context, keys []string) (map[string]*Item, error) { - finishFn := t.setTrace(ctx, "GetMulti", strings.Join(keys, " ")) - items, err := t.Conn.GetMulti(keys) - return items, finishFn(err) -} - -func (t *traceConn) DeleteContext(ctx context.Context, key string) error { - finishFn := t.setTrace(ctx, "Delete", key) - return finishFn(t.Conn.Delete(key)) -} - -func (t *traceConn) IncrementContext(ctx context.Context, key string, delta uint64) (newValue uint64, err error) { - finishFn := t.setTrace(ctx, "Increment", key+" "+strconv.FormatUint(delta, 10)) - newValue, err = t.Conn.Increment(key, delta) - return newValue, finishFn(err) -} - -func (t *traceConn) DecrementContext(ctx context.Context, key string, delta uint64) (newValue uint64, err error) { - finishFn := t.setTrace(ctx, "Decrement", key+" "+strconv.FormatUint(delta, 10)) - newValue, err = t.Conn.Decrement(key, delta) - return newValue, finishFn(err) -} - -func (t *traceConn) CompareAndSwapContext(ctx context.Context, item *Item) error { - finishFn := t.setTrace(ctx, "CompareAndSwap", item.Key) - return finishFn(t.Conn.CompareAndSwap(item)) -} - -func (t *traceConn) TouchContext(ctx context.Context, key string, seconds int32) (err error) { - finishFn := t.setTrace(ctx, "Touch", key+" "+strconv.Itoa(int(seconds))) - return finishFn(t.Conn.Touch(key, seconds)) -} diff --git a/pkg/cache/memcache/util.go b/pkg/cache/memcache/util.go deleted file mode 100644 index e42d49910..000000000 --- a/pkg/cache/memcache/util.go +++ /dev/null @@ -1,88 +0,0 @@ -package memcache - -import ( - "context" - "time" - - "github.com/gogo/protobuf/proto" -) - -func legalKey(key string) bool { - if len(key) > 250 || len(key) == 0 { - return false - } - for i := 0; i < len(key); i++ { - if key[i] <= ' ' || key[i] == 0x7f { - return false - } - } - return true -} - -// MockWith error -func MockWith(err error) Conn { - return errConn{err} -} - -type errConn struct{ err error } - -func (c errConn) Err() error { return c.err } -func (c errConn) Close() error { return c.err } -func (c errConn) Add(*Item) error { return c.err } -func (c errConn) Set(*Item) error { return c.err } -func (c errConn) Replace(*Item) error { return c.err } -func (c errConn) CompareAndSwap(*Item) error { return c.err } -func (c errConn) Get(string) (*Item, error) { return nil, c.err } -func (c errConn) GetMulti([]string) (map[string]*Item, error) { return nil, c.err } -func (c errConn) Touch(string, int32) error { return c.err } -func (c errConn) Delete(string) error { return c.err } -func (c errConn) Increment(string, uint64) (uint64, error) { return 0, c.err } -func (c errConn) Decrement(string, uint64) (uint64, error) { return 0, c.err } -func (c errConn) Scan(*Item, interface{}) error { return c.err } -func (c errConn) AddContext(context.Context, *Item) error { return c.err } -func (c errConn) SetContext(context.Context, *Item) error { return c.err } -func (c errConn) ReplaceContext(context.Context, *Item) error { return c.err } -func (c errConn) GetContext(context.Context, string) (*Item, error) { return nil, c.err } -func (c errConn) DecrementContext(context.Context, string, uint64) (uint64, error) { return 0, c.err } -func (c errConn) CompareAndSwapContext(context.Context, *Item) error { return c.err } -func (c errConn) TouchContext(context.Context, string, int32) error { return c.err } -func (c errConn) DeleteContext(context.Context, string) error { return c.err } -func (c errConn) IncrementContext(context.Context, string, uint64) (uint64, error) { return 0, c.err } -func (c errConn) GetMultiContext(context.Context, []string) (map[string]*Item, error) { - return nil, c.err -} - -// RawItem item with FlagRAW flag. -// -// Expiration is the cache expiration time, in seconds: either a relative -// time from now (up to 1 month), or an absolute Unix epoch time. -// Zero means the Item has no expiration time. -func RawItem(key string, data []byte, flags uint32, expiration int32) *Item { - return &Item{Key: key, Flags: flags | FlagRAW, Value: data, Expiration: expiration} -} - -// JSONItem item with FlagJSON flag. -// -// Expiration is the cache expiration time, in seconds: either a relative -// time from now (up to 1 month), or an absolute Unix epoch time. -// Zero means the Item has no expiration time. -func JSONItem(key string, v interface{}, flags uint32, expiration int32) *Item { - return &Item{Key: key, Flags: flags | FlagJSON, Object: v, Expiration: expiration} -} - -// ProtobufItem item with FlagProtobuf flag. -// -// Expiration is the cache expiration time, in seconds: either a relative -// time from now (up to 1 month), or an absolute Unix epoch time. -// Zero means the Item has no expiration time. -func ProtobufItem(key string, message proto.Message, flags uint32, expiration int32) *Item { - return &Item{Key: key, Flags: flags | FlagProtobuf, Object: message, Expiration: expiration} -} - -func shrinkDeadline(ctx context.Context, timeout time.Duration) time.Time { - timeoutTime := time.Now().Add(timeout) - if deadline, ok := ctx.Deadline(); ok && timeoutTime.After(deadline) { - return deadline - } - return timeoutTime -} diff --git a/pkg/cache/memcache/util_test.go b/pkg/cache/memcache/util_test.go deleted file mode 100644 index f14a888f6..000000000 --- a/pkg/cache/memcache/util_test.go +++ /dev/null @@ -1,105 +0,0 @@ -package memcache - -import ( - "context" - "testing" - "time" - - pb "github.com/go-kratos/kratos/pkg/cache/memcache/test" - - "github.com/stretchr/testify/assert" -) - -func TestItemUtil(t *testing.T) { - item1 := RawItem("test", []byte("hh"), 0, 0) - assert.Equal(t, "test", item1.Key) - assert.Equal(t, []byte("hh"), item1.Value) - assert.Equal(t, FlagRAW, FlagRAW&item1.Flags) - - item1 = JSONItem("test", &Item{}, 0, 0) - assert.Equal(t, "test", item1.Key) - assert.NotNil(t, item1.Object) - assert.Equal(t, FlagJSON, FlagJSON&item1.Flags) - - item1 = ProtobufItem("test", &pb.TestItem{}, 0, 0) - assert.Equal(t, "test", item1.Key) - assert.NotNil(t, item1.Object) - assert.Equal(t, FlagProtobuf, FlagProtobuf&item1.Flags) -} - -func TestLegalKey(t *testing.T) { - type args struct { - key string - } - tests := []struct { - name string - args args - want bool - }{ - { - name: "test empty key", - want: false, - }, - { - name: "test too large key", - args: args{func() string { - var data []byte - for i := 0; i < 255; i++ { - data = append(data, 'k') - } - return string(data) - }()}, - want: false, - }, - { - name: "test invalid char", - args: args{"hello world"}, - want: false, - }, - { - name: "test invalid char", - args: args{string([]byte{0x7f})}, - want: false, - }, - { - name: "test normal key", - args: args{"hello"}, - want: true, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - if got := legalKey(tt.args.key); got != tt.want { - t.Errorf("legalKey() = %v, want %v", got, tt.want) - } - }) - } -} - -func TestShrinkDeadline(t *testing.T) { - t.Run("test not deadline", func(t *testing.T) { - timeout := time.Second - timeoutTime := time.Now().Add(timeout) - tm := shrinkDeadline(context.Background(), timeout) - assert.True(t, tm.After(timeoutTime)) - }) - t.Run("test big deadline", func(t *testing.T) { - timeout := time.Second - timeoutTime := time.Now().Add(timeout) - deadlineTime := time.Now().Add(2 * time.Second) - ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second) - defer cancel() - - tm := shrinkDeadline(ctx, timeout) - assert.True(t, tm.After(timeoutTime) && tm.Before(deadlineTime)) - }) - t.Run("test small deadline", func(t *testing.T) { - timeout := time.Second - deadlineTime := time.Now().Add(500 * time.Millisecond) - ctx, cancel := context.WithTimeout(context.Background(), 500*time.Millisecond) - defer cancel() - - tm := shrinkDeadline(ctx, timeout) - assert.True(t, tm.After(deadlineTime) && tm.Before(time.Now().Add(timeout))) - }) -} diff --git a/pkg/cache/metrics.go b/pkg/cache/metrics.go deleted file mode 100644 index 4c41b429c..000000000 --- a/pkg/cache/metrics.go +++ /dev/null @@ -1,23 +0,0 @@ -package cache - -import "github.com/go-kratos/kratos/pkg/stat/metric" - -const _metricNamespace = "cache" - -// be used in tool/kratos-gen-bts -var ( - MetricHits = metric.NewCounterVec(&metric.CounterVecOpts{ - Namespace: _metricNamespace, - Subsystem: "", - Name: "hits_total", - Help: "cache hits total.", - Labels: []string{"name"}, - }) - MetricMisses = metric.NewCounterVec(&metric.CounterVecOpts{ - Namespace: _metricNamespace, - Subsystem: "", - Name: "misses_total", - Help: "cache misses total.", - Labels: []string{"name"}, - }) -) diff --git a/pkg/cache/redis/README.md b/pkg/cache/redis/README.md deleted file mode 100644 index 588ca73a0..000000000 --- a/pkg/cache/redis/README.md +++ /dev/null @@ -1,7 +0,0 @@ -# cache/redis - -##### 项目简介 -1. 提供redis接口 - -#### 使用方式 -请参考doc.go diff --git a/pkg/cache/redis/commandinfo.go b/pkg/cache/redis/commandinfo.go deleted file mode 100644 index f424eb438..000000000 --- a/pkg/cache/redis/commandinfo.go +++ /dev/null @@ -1,57 +0,0 @@ -// Copyright 2014 Gary Burd -// -// Licensed under the Apache License, Version 2.0 (the "License"): you may -// not use this file except in compliance with the License. You may obtain -// a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -// License for the specific language governing permissions and limitations -// under the License. - -package redis - -import ( - "strings" -) - -// redis state -const ( - WatchState = 1 << iota - MultiState - SubscribeState - MonitorState -) - -// CommandInfo command info. -type CommandInfo struct { - Set, Clear int -} - -var commandInfos = map[string]CommandInfo{ - "WATCH": {Set: WatchState}, - "UNWATCH": {Clear: WatchState}, - "MULTI": {Set: MultiState}, - "EXEC": {Clear: WatchState | MultiState}, - "DISCARD": {Clear: WatchState | MultiState}, - "PSUBSCRIBE": {Set: SubscribeState}, - "SUBSCRIBE": {Set: SubscribeState}, - "MONITOR": {Set: MonitorState}, -} - -func init() { - for n, ci := range commandInfos { - commandInfos[strings.ToLower(n)] = ci - } -} - -// LookupCommandInfo get command info. -func LookupCommandInfo(commandName string) CommandInfo { - if ci, ok := commandInfos[commandName]; ok { - return ci - } - return commandInfos[strings.ToUpper(commandName)] -} diff --git a/pkg/cache/redis/commandinfo_test.go b/pkg/cache/redis/commandinfo_test.go deleted file mode 100644 index d8f4e5214..000000000 --- a/pkg/cache/redis/commandinfo_test.go +++ /dev/null @@ -1,27 +0,0 @@ -package redis - -import "testing" - -func TestLookupCommandInfo(t *testing.T) { - for _, n := range []string{"watch", "WATCH", "wAtch"} { - if LookupCommandInfo(n) == (CommandInfo{}) { - t.Errorf("LookupCommandInfo(%q) = CommandInfo{}, expected non-zero value", n) - } - } -} - -func benchmarkLookupCommandInfo(b *testing.B, names ...string) { - for i := 0; i < b.N; i++ { - for _, c := range names { - LookupCommandInfo(c) - } - } -} - -func BenchmarkLookupCommandInfoCorrectCase(b *testing.B) { - benchmarkLookupCommandInfo(b, "watch", "WATCH", "monitor", "MONITOR") -} - -func BenchmarkLookupCommandInfoMixedCase(b *testing.B) { - benchmarkLookupCommandInfo(b, "wAtch", "WeTCH", "monItor", "MONiTOR") -} diff --git a/pkg/cache/redis/conn.go b/pkg/cache/redis/conn.go deleted file mode 100644 index 949a4bd55..000000000 --- a/pkg/cache/redis/conn.go +++ /dev/null @@ -1,609 +0,0 @@ -// Copyright 2012 Gary Burd -// -// Licensed under the Apache License, Version 2.0 (the "License"): you may -// not use this file except in compliance with the License. You may obtain -// a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -// License for the specific language governing permissions and limitations -// under the License. - -package redis - -import ( - "bufio" - "bytes" - "context" - "fmt" - "io" - "net" - "net/url" - "regexp" - "strconv" - "sync" - "time" - - "github.com/pkg/errors" -) - -// Conn represents a connection to a Redis server. -type Conn interface { - // Close closes the connection. - Close() error - - // Err returns a non-nil value if the connection is broken. The returned - // value is either the first non-nil value returned from the underlying - // network connection or a protocol parsing error. Applications should - // close broken connections. - Err() error - - // Do sends a command to the server and returns the received reply. - Do(commandName string, args ...interface{}) (reply interface{}, err error) - - // Send writes the command to the client's output buffer. - Send(commandName string, args ...interface{}) error - - // Flush flushes the output buffer to the Redis server. - Flush() error - - // Receive receives a single reply from the Redis server - Receive() (reply interface{}, err error) - - // WithContext returns Conn with the input ctx. - WithContext(ctx context.Context) Conn -} - -// conn is the low-level implementation of Conn -type conn struct { - // Shared - mu sync.Mutex - pending int - err error - conn net.Conn - - ctx context.Context - - // Read - readTimeout time.Duration - br *bufio.Reader - - // Write - writeTimeout time.Duration - bw *bufio.Writer - - // Scratch space for formatting argument length. - // '*' or '$', length, "\r\n" - lenScratch [32]byte - - // Scratch space for formatting integers and floats. - numScratch [40]byte -} - -// DialTimeout acts like Dial but takes timeouts for establishing the -// connection to the server, writing a command and reading a reply. -// -// Deprecated: Use Dial with options instead. -func DialTimeout(network, address string, connectTimeout, readTimeout, writeTimeout time.Duration) (Conn, error) { - return Dial(network, address, - DialConnectTimeout(connectTimeout), - DialReadTimeout(readTimeout), - DialWriteTimeout(writeTimeout)) -} - -// DialOption specifies an option for dialing a Redis server. -type DialOption struct { - f func(*dialOptions) -} - -type dialOptions struct { - readTimeout time.Duration - writeTimeout time.Duration - dial func(network, addr string) (net.Conn, error) - db int - password string -} - -// DialReadTimeout specifies the timeout for reading a single command reply. -func DialReadTimeout(d time.Duration) DialOption { - return DialOption{func(do *dialOptions) { - do.readTimeout = d - }} -} - -// DialWriteTimeout specifies the timeout for writing a single command. -func DialWriteTimeout(d time.Duration) DialOption { - return DialOption{func(do *dialOptions) { - do.writeTimeout = d - }} -} - -// DialConnectTimeout specifies the timeout for connecting to the Redis server. -func DialConnectTimeout(d time.Duration) DialOption { - return DialOption{func(do *dialOptions) { - dialer := net.Dialer{Timeout: d} - do.dial = dialer.Dial - }} -} - -// DialNetDial specifies a custom dial function for creating TCP -// connections. If this option is left out, then net.Dial is -// used. DialNetDial overrides DialConnectTimeout. -func DialNetDial(dial func(network, addr string) (net.Conn, error)) DialOption { - return DialOption{func(do *dialOptions) { - do.dial = dial - }} -} - -// DialDatabase specifies the database to select when dialing a connection. -func DialDatabase(db int) DialOption { - return DialOption{func(do *dialOptions) { - do.db = db - }} -} - -// DialPassword specifies the password to use when connecting to -// the Redis server. -func DialPassword(password string) DialOption { - return DialOption{func(do *dialOptions) { - do.password = password - }} -} - -// Dial connects to the Redis server at the given network and -// address using the specified options. -func Dial(network, address string, options ...DialOption) (Conn, error) { - do := dialOptions{ - dial: net.Dial, - } - for _, option := range options { - option.f(&do) - } - - netConn, err := do.dial(network, address) - if err != nil { - return nil, errors.WithStack(err) - } - c := &conn{ - conn: netConn, - bw: bufio.NewWriter(netConn), - br: bufio.NewReader(netConn), - readTimeout: do.readTimeout, - writeTimeout: do.writeTimeout, - } - - if do.password != "" { - if _, err := c.Do("AUTH", do.password); err != nil { - netConn.Close() - return nil, errors.WithStack(err) - } - } - - if do.db != 0 { - if _, err := c.Do("SELECT", do.db); err != nil { - netConn.Close() - return nil, errors.WithStack(err) - } - } - return c, nil -} - -var pathDBRegexp = regexp.MustCompile(`/(\d+)\z`) - -// DialURL connects to a Redis server at the given URL using the Redis -// URI scheme. URLs should follow the draft IANA specification for the -// scheme (https://www.iana.org/assignments/uri-schemes/prov/redis). -func DialURL(rawurl string, options ...DialOption) (Conn, error) { - u, err := url.Parse(rawurl) - if err != nil { - return nil, errors.WithStack(err) - } - - if u.Scheme != "redis" { - return nil, fmt.Errorf("invalid redis URL scheme: %s", u.Scheme) - } - - // As per the IANA draft spec, the host defaults to localhost and - // the port defaults to 6379. - host, port, err := net.SplitHostPort(u.Host) - if err != nil { - // assume port is missing - host = u.Host - port = "6379" - } - if host == "" { - host = "localhost" - } - address := net.JoinHostPort(host, port) - - if u.User != nil { - password, isSet := u.User.Password() - if isSet { - options = append(options, DialPassword(password)) - } - } - - match := pathDBRegexp.FindStringSubmatch(u.Path) - if len(match) == 2 { - db, err := strconv.Atoi(match[1]) - if err != nil { - return nil, errors.Errorf("invalid database: %s", u.Path[1:]) - } - if db != 0 { - options = append(options, DialDatabase(db)) - } - } else if u.Path != "" { - return nil, errors.Errorf("invalid database: %s", u.Path[1:]) - } - - return Dial("tcp", address, options...) -} - -// NewConn new a redis conn. -func NewConn(c *Config) (cn Conn, err error) { - cnop := DialConnectTimeout(time.Duration(c.DialTimeout)) - rdop := DialReadTimeout(time.Duration(c.ReadTimeout)) - wrop := DialWriteTimeout(time.Duration(c.WriteTimeout)) - auop := DialPassword(c.Auth) - // new conn - cn, err = Dial(c.Proto, c.Addr, cnop, rdop, wrop, auop) - return -} - -func (c *conn) Close() error { - c.mu.Lock() - c.ctx = nil - err := c.err - if c.err == nil { - c.err = errors.New("redigo: closed") - err = c.conn.Close() - } - c.mu.Unlock() - return err -} - -func (c *conn) fatal(err error) error { - c.mu.Lock() - if c.err == nil { - c.err = err - // Close connection to force errors on subsequent calls and to unblock - // other reader or writer. - c.conn.Close() - } - c.mu.Unlock() - return errors.WithStack(c.err) -} - -func (c *conn) Err() error { - c.mu.Lock() - err := c.err - c.mu.Unlock() - return err -} - -func (c *conn) writeLen(prefix byte, n int) error { - c.lenScratch[len(c.lenScratch)-1] = '\n' - c.lenScratch[len(c.lenScratch)-2] = '\r' - i := len(c.lenScratch) - 3 - for { - c.lenScratch[i] = byte('0' + n%10) - i-- - n = n / 10 - if n == 0 { - break - } - } - c.lenScratch[i] = prefix - _, err := c.bw.Write(c.lenScratch[i:]) - return errors.WithStack(err) -} - -func (c *conn) writeString(s string) error { - c.writeLen('$', len(s)) - c.bw.WriteString(s) - _, err := c.bw.WriteString("\r\n") - return errors.WithStack(err) -} - -func (c *conn) writeBytes(p []byte) error { - c.writeLen('$', len(p)) - c.bw.Write(p) - _, err := c.bw.WriteString("\r\n") - return errors.WithStack(err) -} - -func (c *conn) writeInt64(n int64) error { - return errors.WithStack(c.writeBytes(strconv.AppendInt(c.numScratch[:0], n, 10))) -} - -func (c *conn) writeFloat64(n float64) error { - return errors.WithStack(c.writeBytes(strconv.AppendFloat(c.numScratch[:0], n, 'g', -1, 64))) -} - -func (c *conn) writeCommand(cmd string, args []interface{}) (err error) { - if c.writeTimeout != 0 { - c.conn.SetWriteDeadline(shrinkDeadline(c.ctx, c.writeTimeout)) - } - c.writeLen('*', 1+len(args)) - err = c.writeString(cmd) - for _, arg := range args { - if err != nil { - break - } - switch arg := arg.(type) { - case string: - err = c.writeString(arg) - case []byte: - err = c.writeBytes(arg) - case int: - err = c.writeInt64(int64(arg)) - case int64: - err = c.writeInt64(arg) - case float64: - err = c.writeFloat64(arg) - case bool: - if arg { - err = c.writeString("1") - } else { - err = c.writeString("0") - } - case nil: - err = c.writeString("") - default: - var buf bytes.Buffer - fmt.Fprint(&buf, arg) - err = errors.WithStack(c.writeBytes(buf.Bytes())) - } - } - return err -} - -type protocolError string - -func (pe protocolError) Error() string { - return fmt.Sprintf("redigo: %s (possible server error or unsupported concurrent read by application)", string(pe)) -} - -func (c *conn) readLine() ([]byte, error) { - p, err := c.br.ReadSlice('\n') - if err == bufio.ErrBufferFull { - return nil, errors.WithStack(protocolError("long response line")) - } - if err != nil { - return nil, err - } - i := len(p) - 2 - if i < 0 || p[i] != '\r' { - return nil, errors.WithStack(protocolError("bad response line terminator")) - } - return p[:i], nil -} - -// parseLen parses bulk string and array lengths. -func parseLen(p []byte) (int, error) { - if len(p) == 0 { - return -1, errors.WithStack(protocolError("malformed length")) - } - - if p[0] == '-' && len(p) == 2 && p[1] == '1' { - // handle $-1 and $-1 null replies. - return -1, nil - } - - var n int - for _, b := range p { - n *= 10 - if b < '0' || b > '9' { - return -1, errors.WithStack(protocolError("illegal bytes in length")) - } - n += int(b - '0') - } - - return n, nil -} - -// parseInt parses an integer reply. -func parseInt(p []byte) (interface{}, error) { - if len(p) == 0 { - return 0, errors.WithStack(protocolError("malformed integer")) - } - - var negate bool - if p[0] == '-' { - negate = true - p = p[1:] - if len(p) == 0 { - return 0, errors.WithStack(protocolError("malformed integer")) - } - } - - var n int64 - for _, b := range p { - n *= 10 - if b < '0' || b > '9' { - return 0, errors.WithStack(protocolError("illegal bytes in length")) - } - n += int64(b - '0') - } - - if negate { - n = -n - } - return n, nil -} - -var ( - okReply interface{} = "OK" - pongReply interface{} = "PONG" -) - -func (c *conn) readReply() (interface{}, error) { - line, err := c.readLine() - if err != nil { - return nil, err - } - if len(line) == 0 { - return nil, errors.WithStack(protocolError("short response line")) - } - switch line[0] { - case '+': - switch { - case len(line) == 3 && line[1] == 'O' && line[2] == 'K': - // Avoid allocation for frequent "+OK" response. - return okReply, nil - case len(line) == 5 && line[1] == 'P' && line[2] == 'O' && line[3] == 'N' && line[4] == 'G': - // Avoid allocation in PING command benchmarks :) - return pongReply, nil - default: - return string(line[1:]), nil - } - case '-': - return Error(string(line[1:])), nil - case ':': - return parseInt(line[1:]) - case '$': - n, err := parseLen(line[1:]) - if n < 0 || err != nil { - return nil, err - } - p := make([]byte, n) - _, err = io.ReadFull(c.br, p) - if err != nil { - return nil, errors.WithStack(err) - } - if line1, err := c.readLine(); err != nil { - return nil, err - } else if len(line1) != 0 { - return nil, errors.WithStack(protocolError("bad bulk string format")) - } - return p, nil - case '*': - n, err := parseLen(line[1:]) - if n < 0 || err != nil { - return nil, err - } - r := make([]interface{}, n) - for i := range r { - r[i], err = c.readReply() - if err != nil { - return nil, err - } - } - return r, nil - } - return nil, errors.WithStack(protocolError("unexpected response line")) -} -func (c *conn) Send(cmd string, args ...interface{}) (err error) { - c.mu.Lock() - c.pending++ - c.mu.Unlock() - if err = c.writeCommand(cmd, args); err != nil { - c.fatal(err) - } - return err -} - -func (c *conn) Flush() (err error) { - if c.writeTimeout != 0 { - c.conn.SetWriteDeadline(shrinkDeadline(c.ctx, c.writeTimeout)) - } - if err = c.bw.Flush(); err != nil { - c.fatal(err) - } - return err -} - -func (c *conn) Receive() (reply interface{}, err error) { - if c.readTimeout != 0 { - c.conn.SetReadDeadline(shrinkDeadline(c.ctx, c.readTimeout)) - } - if reply, err = c.readReply(); err != nil { - return nil, c.fatal(err) - } - // When using pub/sub, the number of receives can be greater than the - // number of sends. To enable normal use of the connection after - // unsubscribing from all channels, we do not decrement pending to a - // negative value. - // - // The pending field is decremented after the reply is read to handle the - // case where Receive is called before Send. - c.mu.Lock() - if c.pending > 0 { - c.pending-- - } - c.mu.Unlock() - if err, ok := reply.(Error); ok { - return nil, err - } - return -} - -func (c *conn) Do(cmd string, args ...interface{}) (reply interface{}, err error) { - c.mu.Lock() - pending := c.pending - c.pending = 0 - c.mu.Unlock() - if cmd == "" && pending == 0 { - return nil, nil - } - - if cmd != "" { - err = c.writeCommand(cmd, args) - } - if err == nil { - err = errors.WithStack(c.bw.Flush()) - } - if err != nil { - return nil, c.fatal(err) - } - if c.readTimeout != 0 { - c.conn.SetReadDeadline(shrinkDeadline(c.ctx, c.readTimeout)) - } - if cmd == "" { - reply := make([]interface{}, pending) - for i := range reply { - var r interface{} - r, err = c.readReply() - if err != nil { - break - } - reply[i] = r - } - if err != nil { - return nil, c.fatal(err) - } - return reply, nil - } - - for i := 0; i <= pending; i++ { - var e error - if reply, e = c.readReply(); e != nil { - return nil, c.fatal(e) - } - if e, ok := reply.(Error); ok && err == nil { - err = e - } - } - return reply, err -} - -func (c *conn) copy() *conn { - return &conn{ - pending: c.pending, - err: c.err, - conn: c.conn, - bw: c.bw, - br: c.br, - readTimeout: c.readTimeout, - writeTimeout: c.writeTimeout, - } -} - -func (c *conn) WithContext(ctx context.Context) Conn { - c2 := c.copy() - c2.ctx = ctx - return c2 -} diff --git a/pkg/cache/redis/conn_test.go b/pkg/cache/redis/conn_test.go deleted file mode 100644 index 3e37e882c..000000000 --- a/pkg/cache/redis/conn_test.go +++ /dev/null @@ -1,670 +0,0 @@ -// Copyright 2012 Gary Burd -// -// Licensed under the Apache License, Version 2.0 (the "License"): you may -// not use this file except in compliance with the License. You may obtain -// a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -// License for the specific language governing permissions and limitations -// under the License. - -package redis - -import ( - "bytes" - "context" - "io" - "math" - "net" - "os" - "reflect" - "strings" - "testing" - "time" -) - -type tConn struct { - io.Reader - io.Writer -} - -func (*tConn) Close() error { return nil } -func (*tConn) LocalAddr() net.Addr { return nil } -func (*tConn) RemoteAddr() net.Addr { return nil } -func (*tConn) SetDeadline(t time.Time) error { return nil } -func (*tConn) SetReadDeadline(t time.Time) error { return nil } -func (*tConn) SetWriteDeadline(t time.Time) error { return nil } - -func dialTestConn(r io.Reader, w io.Writer) DialOption { - return DialNetDial(func(net, addr string) (net.Conn, error) { - return &tConn{Reader: r, Writer: w}, nil - }) -} - -var writeTests = []struct { - args []interface{} - expected string -}{ - { - []interface{}{"SET", "key", "value"}, - "*3\r\n$3\r\nSET\r\n$3\r\nkey\r\n$5\r\nvalue\r\n", - }, - { - []interface{}{"SET", "key", "value"}, - "*3\r\n$3\r\nSET\r\n$3\r\nkey\r\n$5\r\nvalue\r\n", - }, - { - []interface{}{"SET", "key", byte(100)}, - "*3\r\n$3\r\nSET\r\n$3\r\nkey\r\n$3\r\n100\r\n", - }, - { - []interface{}{"SET", "key", 100}, - "*3\r\n$3\r\nSET\r\n$3\r\nkey\r\n$3\r\n100\r\n", - }, - { - []interface{}{"SET", "key", int64(math.MinInt64)}, - "*3\r\n$3\r\nSET\r\n$3\r\nkey\r\n$20\r\n-9223372036854775808\r\n", - }, - { - []interface{}{"SET", "key", float64(1349673917.939762)}, - "*3\r\n$3\r\nSET\r\n$3\r\nkey\r\n$21\r\n1.349673917939762e+09\r\n", - }, - { - []interface{}{"SET", "key", ""}, - "*3\r\n$3\r\nSET\r\n$3\r\nkey\r\n$0\r\n\r\n", - }, - { - []interface{}{"SET", "key", nil}, - "*3\r\n$3\r\nSET\r\n$3\r\nkey\r\n$0\r\n\r\n", - }, - { - []interface{}{"ECHO", true, false}, - "*3\r\n$4\r\nECHO\r\n$1\r\n1\r\n$1\r\n0\r\n", - }, -} - -func TestWrite(t *testing.T) { - for _, tt := range writeTests { - var buf bytes.Buffer - c, _ := Dial("", "", dialTestConn(nil, &buf)) - err := c.Send(tt.args[0].(string), tt.args[1:]...) - if err != nil { - t.Errorf("Send(%v) returned error %v", tt.args, err) - continue - } - c.Flush() - actual := buf.String() - if actual != tt.expected { - t.Errorf("Send(%v) = %q, want %q", tt.args, actual, tt.expected) - } - } -} - -var errorSentinel = &struct{}{} - -var readTests = []struct { - reply string - expected interface{} -}{ - { - "+OK\r\n", - "OK", - }, - { - "+PONG\r\n", - "PONG", - }, - { - "@OK\r\n", - errorSentinel, - }, - { - "$6\r\nfoobar\r\n", - []byte("foobar"), - }, - { - "$-1\r\n", - nil, - }, - { - ":1\r\n", - int64(1), - }, - { - ":-2\r\n", - int64(-2), - }, - { - "*0\r\n", - []interface{}{}, - }, - { - "*-1\r\n", - nil, - }, - { - "*4\r\n$3\r\nfoo\r\n$3\r\nbar\r\n$5\r\nHello\r\n$5\r\nWorld\r\n", - []interface{}{[]byte("foo"), []byte("bar"), []byte("Hello"), []byte("World")}, - }, - { - "*3\r\n$3\r\nfoo\r\n$-1\r\n$3\r\nbar\r\n", - []interface{}{[]byte("foo"), nil, []byte("bar")}, - }, - - { - // "x" is not a valid length - "$x\r\nfoobar\r\n", - errorSentinel, - }, - { - // -2 is not a valid length - "$-2\r\n", - errorSentinel, - }, - { - // "x" is not a valid integer - ":x\r\n", - errorSentinel, - }, - { - // missing \r\n following value - "$6\r\nfoobar", - errorSentinel, - }, - { - // short value - "$6\r\nxx", - errorSentinel, - }, - { - // long value - "$6\r\nfoobarx\r\n", - errorSentinel, - }, -} - -func TestRead(t *testing.T) { - for _, tt := range readTests { - c, _ := Dial("", "", dialTestConn(strings.NewReader(tt.reply), nil)) - actual, err := c.Receive() - if tt.expected == errorSentinel { - if err == nil { - t.Errorf("Receive(%q) did not return expected error", tt.reply) - } - } else { - if err != nil { - t.Errorf("Receive(%q) returned error %v", tt.reply, err) - continue - } - if !reflect.DeepEqual(actual, tt.expected) { - t.Errorf("Receive(%q) = %v, want %v", tt.reply, actual, tt.expected) - } - } - } -} - -var testCommands = []struct { - args []interface{} - expected interface{} -}{ - { - []interface{}{"PING"}, - "PONG", - }, - { - []interface{}{"SET", "foo", "bar"}, - "OK", - }, - { - []interface{}{"GET", "foo"}, - []byte("bar"), - }, - { - []interface{}{"GET", "nokey"}, - nil, - }, - { - []interface{}{"MGET", "nokey", "foo"}, - []interface{}{nil, []byte("bar")}, - }, - { - []interface{}{"INCR", "mycounter"}, - int64(1), - }, - { - []interface{}{"LPUSH", "mylist", "foo"}, - int64(1), - }, - { - []interface{}{"LPUSH", "mylist", "bar"}, - int64(2), - }, - { - []interface{}{"LRANGE", "mylist", 0, -1}, - []interface{}{[]byte("bar"), []byte("foo")}, - }, - { - []interface{}{"MULTI"}, - "OK", - }, - { - []interface{}{"LRANGE", "mylist", 0, -1}, - "QUEUED", - }, - { - []interface{}{"PING"}, - "QUEUED", - }, - { - []interface{}{"EXEC"}, - []interface{}{ - []interface{}{[]byte("bar"), []byte("foo")}, - "PONG", - }, - }, -} - -func TestDoCommands(t *testing.T) { - c, err := DialDefaultServer() - if err != nil { - t.Fatalf("error connection to database, %v", err) - } - defer c.Close() - - for _, cmd := range testCommands { - actual, err := c.Do(cmd.args[0].(string), cmd.args[1:]...) - if err != nil { - t.Errorf("Do(%v) returned error %v", cmd.args, err) - continue - } - if !reflect.DeepEqual(actual, cmd.expected) { - t.Errorf("Do(%v) = %v, want %v", cmd.args, actual, cmd.expected) - } - } -} - -func TestPipelineCommands(t *testing.T) { - c, err := DialDefaultServer() - if err != nil { - t.Fatalf("error connection to database, %v", err) - } - defer c.Close() - - for _, cmd := range testCommands { - if err := c.Send(cmd.args[0].(string), cmd.args[1:]...); err != nil { - t.Fatalf("Send(%v) returned error %v", cmd.args, err) - } - } - if err := c.Flush(); err != nil { - t.Errorf("Flush() returned error %v", err) - } - for _, cmd := range testCommands { - actual, err := c.Receive() - if err != nil { - t.Fatalf("Receive(%v) returned error %v", cmd.args, err) - } - if !reflect.DeepEqual(actual, cmd.expected) { - t.Errorf("Receive(%v) = %v, want %v", cmd.args, actual, cmd.expected) - } - } -} - -func TestBlankCommmand(t *testing.T) { - c, err := DialDefaultServer() - if err != nil { - t.Fatalf("error connection to database, %v", err) - } - defer c.Close() - - for _, cmd := range testCommands { - if err = c.Send(cmd.args[0].(string), cmd.args[1:]...); err != nil { - t.Fatalf("Send(%v) returned error %v", cmd.args, err) - } - } - reply, err := Values(c.Do("")) - if err != nil { - t.Fatalf("Do() returned error %v", err) - } - if len(reply) != len(testCommands) { - t.Fatalf("len(reply)=%d, want %d", len(reply), len(testCommands)) - } - for i, cmd := range testCommands { - actual := reply[i] - if !reflect.DeepEqual(actual, cmd.expected) { - t.Errorf("Receive(%v) = %v, want %v", cmd.args, actual, cmd.expected) - } - } -} - -func TestRecvBeforeSend(t *testing.T) { - c, err := DialDefaultServer() - if err != nil { - t.Fatalf("error connection to database, %v", err) - } - defer c.Close() - done := make(chan struct{}) - go func() { - c.Receive() - close(done) - }() - time.Sleep(time.Millisecond) - c.Send("PING") - c.Flush() - <-done - _, err = c.Do("") - if err != nil { - t.Fatalf("error=%v", err) - } -} - -func TestError(t *testing.T) { - c, err := DialDefaultServer() - if err != nil { - t.Fatalf("error connection to database, %v", err) - } - defer c.Close() - - c.Do("SET", "key", "val") - _, err = c.Do("HSET", "key", "fld", "val") - if err == nil { - t.Errorf("Expected err for HSET on string key.") - } - if c.Err() != nil { - t.Errorf("Conn has Err()=%v, expect nil", c.Err()) - } - _, err = c.Do("SET", "key", "val") - if err != nil { - t.Errorf("Do(SET, key, val) returned error %v, expected nil.", err) - } -} - -func TestReadTimeout(t *testing.T) { - l, err := net.Listen("tcp", "127.0.0.1:0") - if err != nil { - t.Fatalf("net.Listen returned %v", err) - } - defer l.Close() - - go func() { - for { - c, err1 := l.Accept() - if err1 != nil { - return - } - go func() { - time.Sleep(time.Second) - c.Write([]byte("+OK\r\n")) - c.Close() - }() - } - }() - - // Do - - c1, err := Dial(l.Addr().Network(), l.Addr().String(), DialReadTimeout(time.Millisecond)) - if err != nil { - t.Fatalf("Dial returned %v", err) - } - defer c1.Close() - - _, err = c1.Do("PING") - if err == nil { - t.Fatalf("c1.Do() returned nil, expect error") - } - if c1.Err() == nil { - t.Fatalf("c1.Err() = nil, expect error") - } - - // Send/Flush/Receive - - c2, err := Dial(l.Addr().Network(), l.Addr().String(), DialReadTimeout(time.Millisecond)) - if err != nil { - t.Fatalf("Dial returned %v", err) - } - defer c2.Close() - - c2.Send("PING") - c2.Flush() - _, err = c2.Receive() - if err == nil { - t.Fatalf("c2.Receive() returned nil, expect error") - } - if c2.Err() == nil { - t.Fatalf("c2.Err() = nil, expect error") - } -} - -var dialErrors = []struct { - rawurl string - expectedError string -}{ - { - "localhost", - "invalid redis URL scheme", - }, - // The error message for invalid hosts is diffferent in different - // versions of Go, so just check that there is an error message. - { - "redis://weird url", - "", - }, - { - "redis://foo:bar:baz", - "", - }, - { - "http://www.google.com", - "invalid redis URL scheme: http", - }, - { - "redis://localhost:6379/abc123", - "invalid database: abc123", - }, -} - -func TestDialURLErrors(t *testing.T) { - for _, d := range dialErrors { - _, err := DialURL(d.rawurl) - if err == nil || !strings.Contains(err.Error(), d.expectedError) { - t.Errorf("DialURL did not return expected error (expected %v to contain %s)", err, d.expectedError) - } - } -} - -func TestDialURLPort(t *testing.T) { - checkPort := func(network, address string) (net.Conn, error) { - if address != "localhost:6379" { - t.Errorf("DialURL did not set port to 6379 by default (got %v)", address) - } - return nil, nil - } - _, err := DialURL("redis://localhost", DialNetDial(checkPort)) - if err != nil { - t.Error("dial error:", err) - } -} - -func TestDialURLHost(t *testing.T) { - checkHost := func(network, address string) (net.Conn, error) { - if address != "localhost:6379" { - t.Errorf("DialURL did not set host to localhost by default (got %v)", address) - } - return nil, nil - } - _, err := DialURL("redis://:6379", DialNetDial(checkHost)) - if err != nil { - t.Error("dial error:", err) - } -} - -func TestDialURLPassword(t *testing.T) { - var buf bytes.Buffer - _, err := DialURL("redis://x:abc123@localhost", dialTestConn(strings.NewReader("+OK\r\n"), &buf)) - if err != nil { - t.Error("dial error:", err) - } - expected := "*2\r\n$4\r\nAUTH\r\n$6\r\nabc123\r\n" - actual := buf.String() - if actual != expected { - t.Errorf("commands = %q, want %q", actual, expected) - } -} - -func TestDialURLDatabase(t *testing.T) { - var buf bytes.Buffer - _, err := DialURL("redis://localhost/3", dialTestConn(strings.NewReader("+OK\r\n"), &buf)) - if err != nil { - t.Error("dial error:", err) - } - expected := "*2\r\n$6\r\nSELECT\r\n$1\r\n3\r\n" - actual := buf.String() - if actual != expected { - t.Errorf("commands = %q, want %q", actual, expected) - } -} - -// Connect to local instance of Redis running on the default port. -func ExampleDial() { - c, err := Dial("tcp", ":6379") - if err != nil { - // handle error - } - defer c.Close() -} - -// Connect to remote instance of Redis using a URL. -func ExampleDialURL() { - c, err := DialURL(os.Getenv("REDIS_URL")) - if err != nil { - // handle connection error - } - defer c.Close() -} - -// TextExecError tests handling of errors in a transaction. See -// http://io/topics/transactions for information on how Redis handles -// errors in a transaction. -func TestExecError(t *testing.T) { - c, err := DialDefaultServer() - if err != nil { - t.Fatalf("error connection to database, %v", err) - } - defer c.Close() - - // Execute commands that fail before EXEC is called. - - c.Do("DEL", "k0") - c.Do("ZADD", "k0", 0, 0) - c.Send("MULTI") - c.Send("NOTACOMMAND", "k0", 0, 0) - c.Send("ZINCRBY", "k0", 0, 0) - v, err := c.Do("EXEC") - if err == nil { - t.Fatalf("EXEC returned values %v, expected error", v) - } - - // Execute commands that fail after EXEC is called. The first command - // returns an error. - - c.Do("DEL", "k1") - c.Do("ZADD", "k1", 0, 0) - c.Send("MULTI") - c.Send("HSET", "k1", 0, 0) - c.Send("ZINCRBY", "k1", 0, 0) - v, err = c.Do("EXEC") - if err != nil { - t.Fatalf("EXEC returned error %v", err) - } - - vs, err := Values(v, nil) - if err != nil { - t.Fatalf("Values(v) returned error %v", err) - } - - if len(vs) != 2 { - t.Fatalf("len(vs) == %d, want 2", len(vs)) - } - - if _, ok := vs[0].(error); !ok { - t.Fatalf("first result is type %T, expected error", vs[0]) - } - - if _, ok := vs[1].([]byte); !ok { - t.Fatalf("second result is type %T, expected []byte", vs[1]) - } - - // Execute commands that fail after EXEC is called. The second command - // returns an error. - - c.Do("ZADD", "k2", 0, 0) - c.Send("MULTI") - c.Send("ZINCRBY", "k2", 0, 0) - c.Send("HSET", "k2", 0, 0) - v, err = c.Do("EXEC") - if err != nil { - t.Fatalf("EXEC returned error %v", err) - } - - vs, err = Values(v, nil) - if err != nil { - t.Fatalf("Values(v) returned error %v", err) - } - - if len(vs) != 2 { - t.Fatalf("len(vs) == %d, want 2", len(vs)) - } - - if _, ok := vs[0].([]byte); !ok { - t.Fatalf("first result is type %T, expected []byte", vs[0]) - } - - if _, ok := vs[1].(error); !ok { - t.Fatalf("second result is type %T, expected error", vs[2]) - } -} - -func BenchmarkDoEmpty(b *testing.B) { - c, err := DialDefaultServer() - if err != nil { - b.Fatal(err) - } - defer c.Close() - b.ResetTimer() - for i := 0; i < b.N; i++ { - if _, err := c.Do(""); err != nil { - b.Fatal(err) - } - } -} - -func BenchmarkDoPing(b *testing.B) { - c, err := DialDefaultServer() - if err != nil { - b.Fatal(err) - } - defer c.Close() - b.ResetTimer() - for i := 0; i < b.N; i++ { - if _, err := c.Do("PING"); err != nil { - b.Fatal(err) - } - } -} - -func BenchmarkConn(b *testing.B) { - for i := 0; i < b.N; i++ { - c, err := DialDefaultServer() - if err != nil { - b.Fatal(err) - } - c2 := c.WithContext(context.TODO()) - if _, err := c2.Do("PING"); err != nil { - b.Fatal(err) - } - c2.Close() - } -} diff --git a/pkg/cache/redis/doc.go b/pkg/cache/redis/doc.go deleted file mode 100644 index 1ae6f0cc2..000000000 --- a/pkg/cache/redis/doc.go +++ /dev/null @@ -1,169 +0,0 @@ -// Copyright 2012 Gary Burd -// -// Licensed under the Apache License, Version 2.0 (the "License"): you may -// not use this file except in compliance with the License. You may obtain -// a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -// License for the specific language governing permissions and limitations -// under the License. - -// Package redis is a client for the Redis database. -// -// The Redigo FAQ (https://github.com/garyburd/redigo/wiki/FAQ) contains more -// documentation about this package. -// -// Connections -// -// The Conn interface is the primary interface for working with Redis. -// Applications create connections by calling the Dial, DialWithTimeout or -// NewConn functions. In the future, functions will be added for creating -// sharded and other types of connections. -// -// The application must call the connection Close method when the application -// is done with the connection. -// -// Executing Commands -// -// The Conn interface has a generic method for executing Redis commands: -// -// Do(commandName string, args ...interface{}) (reply interface{}, err error) -// -// The Redis command reference (http://redis.io/commands) lists the available -// commands. An example of using the Redis APPEND command is: -// -// n, err := conn.Do("APPEND", "key", "value") -// -// The Do method converts command arguments to binary strings for transmission -// to the server as follows: -// -// Go Type Conversion -// []byte Sent as is -// string Sent as is -// int, int64 strconv.FormatInt(v) -// float64 strconv.FormatFloat(v, 'g', -1, 64) -// bool true -> "1", false -> "0" -// nil "" -// all other types fmt.Print(v) -// -// Redis command reply types are represented using the following Go types: -// -// Redis type Go type -// error redis.Error -// integer int64 -// simple string string -// bulk string []byte or nil if value not present. -// array []interface{} or nil if value not present. -// -// Use type assertions or the reply helper functions to convert from -// interface{} to the specific Go type for the command result. -// -// Pipelining -// -// Connections support pipelining using the Send, Flush and Receive methods. -// -// Send(commandName string, args ...interface{}) error -// Flush() error -// Receive() (reply interface{}, err error) -// -// Send writes the command to the connection's output buffer. Flush flushes the -// connection's output buffer to the server. Receive reads a single reply from -// the server. The following example shows a simple pipeline. -// -// c.Send("SET", "foo", "bar") -// c.Send("GET", "foo") -// c.Flush() -// c.Receive() // reply from SET -// v, err = c.Receive() // reply from GET -// -// The Do method combines the functionality of the Send, Flush and Receive -// methods. The Do method starts by writing the command and flushing the output -// buffer. Next, the Do method receives all pending replies including the reply -// for the command just sent by Do. If any of the received replies is an error, -// then Do returns the error. If there are no errors, then Do returns the last -// reply. If the command argument to the Do method is "", then the Do method -// will flush the output buffer and receive pending replies without sending a -// command. -// -// Use the Send and Do methods to implement pipelined transactions. -// -// c.Send("MULTI") -// c.Send("INCR", "foo") -// c.Send("INCR", "bar") -// r, err := c.Do("EXEC") -// fmt.Println(r) // prints [1, 1] -// -// Concurrency -// -// Connections do not support concurrent calls to the write methods (Send, -// Flush) or concurrent calls to the read method (Receive). Connections do -// allow a concurrent reader and writer. -// -// Because the Do method combines the functionality of Send, Flush and Receive, -// the Do method cannot be called concurrently with the other methods. -// -// For full concurrent access to Redis, use the thread-safe Pool to get and -// release connections from within a goroutine. -// -// Publish and Subscribe -// -// Use the Send, Flush and Receive methods to implement Pub/Sub subscribers. -// -// c.Send("SUBSCRIBE", "example") -// c.Flush() -// for { -// reply, err := c.Receive() -// if err != nil { -// return err -// } -// // process pushed message -// } -// -// The PubSubConn type wraps a Conn with convenience methods for implementing -// subscribers. The Subscribe, PSubscribe, Unsubscribe and PUnsubscribe methods -// send and flush a subscription management command. The receive method -// converts a pushed message to convenient types for use in a type switch. -// -// psc := redis.PubSubConn{c} -// psc.Subscribe("example") -// for { -// switch v := psc.Receive().(type) { -// case redis.Message: -// fmt.Printf("%s: message: %s\n", v.Channel, v.Data) -// case redis.Subscription: -// fmt.Printf("%s: %s %d\n", v.Channel, v.Kind, v.Count) -// case error: -// return v -// } -// } -// -// Reply Helpers -// -// The Bool, Int, Bytes, String, Strings and Values functions convert a reply -// to a value of a specific type. To allow convenient wrapping of calls to the -// connection Do and Receive methods, the functions take a second argument of -// type error. If the error is non-nil, then the helper function returns the -// error. If the error is nil, the function converts the reply to the specified -// type: -// -// exists, err := redis.Bool(c.Do("EXISTS", "foo")) -// if err != nil { -// // handle error return from c.Do or type conversion error. -// } -// -// The Scan function converts elements of a array reply to Go types: -// -// var value1 int -// var value2 string -// reply, err := redis.Values(c.Do("MGET", "key1", "key2")) -// if err != nil { -// // handle error -// } -// if _, err := redis.Scan(reply, &value1, &value2); err != nil { -// // handle error -// } -package redis diff --git a/pkg/cache/redis/errors.go b/pkg/cache/redis/errors.go deleted file mode 100644 index 6be483ff9..000000000 --- a/pkg/cache/redis/errors.go +++ /dev/null @@ -1,43 +0,0 @@ -package redis - -import ( - "strings" - - pkgerr "github.com/pkg/errors" -) - -func formatErr(err error, name, addr string) string { - e := pkgerr.Cause(err) - switch e { - case ErrNil, nil: - if e == ErrNil { - _metricMisses.Inc(name, addr) - } - return "" - default: - es := e.Error() - switch { - case strings.HasPrefix(es, "read"): - return "read timeout" - case strings.HasPrefix(es, "dial"): - if strings.Contains(es, "connection refused") { - return "connection refused" - } - return "dial timeout" - case strings.HasPrefix(es, "write"): - return "write timeout" - case strings.Contains(es, "EOF"): - return "eof" - case strings.Contains(es, "reset"): - return "reset" - case strings.Contains(es, "broken"): - return "broken pipe" - case strings.Contains(es, "pool exhausted"): - return "pool exhausted" - case strings.Contains(es, "pool closed"): - return "pool closed" - default: - return "unexpected err" - } - } -} diff --git a/pkg/cache/redis/log.go b/pkg/cache/redis/log.go deleted file mode 100644 index 487a1408f..000000000 --- a/pkg/cache/redis/log.go +++ /dev/null @@ -1,123 +0,0 @@ -// Copyright 2012 Gary Burd -// -// Licensed under the Apache License, Version 2.0 (the "License"): you may -// not use this file except in compliance with the License. You may obtain -// a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -// License for the specific language governing permissions and limitations -// under the License. - -package redis - -import ( - "bytes" - "context" - "fmt" - "log" -) - -// NewLoggingConn returns a logging wrapper around a connection. -// ATTENTION: ONLY use loggingConn in developing, DO NOT use this in production. -func NewLoggingConn(conn Conn, logger *log.Logger, prefix string) Conn { - if prefix != "" { - prefix = prefix + "." - } - return &loggingConn{Conn: conn, logger: logger, prefix: prefix} -} - -type loggingConn struct { - Conn - logger *log.Logger - prefix string -} - -func (c *loggingConn) Close() error { - err := c.Conn.Close() - var buf bytes.Buffer - fmt.Fprintf(&buf, "%sClose() -> (%v)", c.prefix, err) - c.logger.Output(2, buf.String()) - return err -} - -func (c *loggingConn) printValue(buf *bytes.Buffer, v interface{}) { - const chop = 32 - switch v := v.(type) { - case []byte: - if len(v) > chop { - fmt.Fprintf(buf, "%q...", v[:chop]) - } else { - fmt.Fprintf(buf, "%q", v) - } - case string: - if len(v) > chop { - fmt.Fprintf(buf, "%q...", v[:chop]) - } else { - fmt.Fprintf(buf, "%q", v) - } - case []interface{}: - if len(v) == 0 { - buf.WriteString("[]") - } else { - sep := "[" - fin := "]" - if len(v) > chop { - v = v[:chop] - fin = "...]" - } - for _, vv := range v { - buf.WriteString(sep) - c.printValue(buf, vv) - sep = ", " - } - buf.WriteString(fin) - } - default: - fmt.Fprint(buf, v) - } -} - -func (c *loggingConn) print(method, commandName string, args []interface{}, reply interface{}, err error) { - var buf bytes.Buffer - fmt.Fprintf(&buf, "%s%s(", c.prefix, method) - if method != "Receive" { - buf.WriteString(commandName) - for _, arg := range args { - buf.WriteString(", ") - c.printValue(&buf, arg) - } - } - buf.WriteString(") -> (") - if method != "Send" { - c.printValue(&buf, reply) - buf.WriteString(", ") - } - fmt.Fprintf(&buf, "%v)", err) - c.logger.Output(3, buf.String()) -} - -func (c *loggingConn) Do(commandName string, args ...interface{}) (reply interface{}, err error) { - reply, err = c.Conn.Do(commandName, args...) - c.print("Do", commandName, args, reply, err) - return reply, err -} - -func (c *loggingConn) Send(commandName string, args ...interface{}) (err error) { - err = c.Conn.Send(commandName, args...) - c.print("Send", commandName, args, nil, err) - return -} - -func (c *loggingConn) Receive() (interface{}, error) { - reply, err := c.Conn.Receive() - c.print("Receive", "", nil, reply, err) - return reply, err -} - -func (c *loggingConn) WithContext(ctx context.Context) Conn { - return c -} diff --git a/pkg/cache/redis/main_test.go b/pkg/cache/redis/main_test.go deleted file mode 100644 index 0cf3556b5..000000000 --- a/pkg/cache/redis/main_test.go +++ /dev/null @@ -1,67 +0,0 @@ -package redis - -import ( - "flag" - "os" - "testing" - "time" - - "github.com/go-kratos/kratos/pkg/container/pool" - "github.com/go-kratos/kratos/pkg/testing/lich" - xtime "github.com/go-kratos/kratos/pkg/time" -) - -var ( - testRedisAddr string - testPool *Pool - testConfig *Config -) - -func setupTestConfig(addr string) { - c := getTestConfig(addr) - c.Config = &pool.Config{ - Active: 20, - Idle: 2, - IdleTimeout: xtime.Duration(90 * time.Second), - } - testConfig = c -} - -func getTestConfig(addr string) *Config { - return &Config{ - Name: "test", - Proto: "tcp", - Addr: addr, - DialTimeout: xtime.Duration(time.Second), - ReadTimeout: xtime.Duration(time.Second), - WriteTimeout: xtime.Duration(time.Second), - } -} - -func setupTestPool() { - testPool = NewPool(testConfig) -} - -// DialDefaultServer starts the test server if not already started and dials a -// connection to the server. -func DialDefaultServer() (Conn, error) { - c, err := Dial("tcp", testRedisAddr, DialReadTimeout(1*time.Second), DialWriteTimeout(1*time.Second)) - if err != nil { - return nil, err - } - c.Do("FLUSHDB") - return c, nil -} - -func TestMain(m *testing.M) { - flag.Set("f", "./test/docker-compose.yaml") - if err := lich.Setup(); err != nil { - panic(err) - } - defer lich.Teardown() - testRedisAddr = "localhost:6379" - setupTestConfig(testRedisAddr) - setupTestPool() - ret := m.Run() - os.Exit(ret) -} diff --git a/pkg/cache/redis/metrics.go b/pkg/cache/redis/metrics.go deleted file mode 100644 index 4ad7a0b87..000000000 --- a/pkg/cache/redis/metrics.go +++ /dev/null @@ -1,53 +0,0 @@ -package redis - -import ( - "github.com/go-kratos/kratos/pkg/stat/metric" -) - -const namespace = "redis_client" - -var ( - _metricReqDur = metric.NewHistogramVec(&metric.HistogramVecOpts{ - Namespace: namespace, - Subsystem: "requests", - Name: "duration_ms", - Help: "redis client requests duration(ms).", - Labels: []string{"name", "addr", "command"}, - Buckets: []float64{5, 10, 25, 50, 100, 250, 500, 1000, 2500}, - }) - _metricReqErr = metric.NewCounterVec(&metric.CounterVecOpts{ - Namespace: namespace, - Subsystem: "requests", - Name: "error_total", - Help: "redis client requests error count.", - Labels: []string{"name", "addr", "command", "error"}, - }) - _metricConnTotal = metric.NewCounterVec(&metric.CounterVecOpts{ - Namespace: namespace, - Subsystem: "connections", - Name: "total", - Help: "redis client connections total count.", - Labels: []string{"name", "addr", "state"}, - }) - _metricConnCurrent = metric.NewGaugeVec(&metric.GaugeVecOpts{ - Namespace: namespace, - Subsystem: "connections", - Name: "current", - Help: "redis client connections current.", - Labels: []string{"name", "addr", "state"}, - }) - _metricHits = metric.NewCounterVec(&metric.CounterVecOpts{ - Namespace: namespace, - Subsystem: "", - Name: "hits_total", - Help: "redis client hits total.", - Labels: []string{"name", "addr"}, - }) - _metricMisses = metric.NewCounterVec(&metric.CounterVecOpts{ - Namespace: namespace, - Subsystem: "", - Name: "misses_total", - Help: "redis client misses total.", - Labels: []string{"name", "addr"}, - }) -) diff --git a/pkg/cache/redis/mock.go b/pkg/cache/redis/mock.go deleted file mode 100644 index 55ed57447..000000000 --- a/pkg/cache/redis/mock.go +++ /dev/null @@ -1,36 +0,0 @@ -package redis - -import "context" - -// MockErr for unit test. -type MockErr struct { - Error error -} - -// MockWith return a mock conn. -func MockWith(err error) MockErr { - return MockErr{Error: err} -} - -// Err . -func (m MockErr) Err() error { return m.Error } - -// Close . -func (m MockErr) Close() error { return m.Error } - -// Do . -func (m MockErr) Do(commandName string, args ...interface{}) (interface{}, error) { - return nil, m.Error -} - -// Send . -func (m MockErr) Send(commandName string, args ...interface{}) error { return m.Error } - -// Flush . -func (m MockErr) Flush() error { return m.Error } - -// Receive . -func (m MockErr) Receive() (interface{}, error) { return nil, m.Error } - -// WithContext . -func (m MockErr) WithContext(context.Context) Conn { return m } diff --git a/pkg/cache/redis/pipeline.go b/pkg/cache/redis/pipeline.go deleted file mode 100644 index e9e745a26..000000000 --- a/pkg/cache/redis/pipeline.go +++ /dev/null @@ -1,84 +0,0 @@ -package redis - -import ( - "context" - "errors" -) - -type Pipeliner interface { - // Send writes the command to the client's output buffer. - Send(commandName string, args ...interface{}) - - // Exec executes all commands and get replies. - Exec(ctx context.Context) (rs *Replies, err error) -} - -var ( - ErrNoReply = errors.New("redis: no reply in result set") -) - -type pipeliner struct { - pool *Pool - cmds []*cmd -} - -type Replies struct { - replies []*reply -} - -type reply struct { - reply interface{} - err error -} - -func (rs *Replies) Next() bool { - return len(rs.replies) > 0 -} - -func (rs *Replies) Scan() (reply interface{}, err error) { - if !rs.Next() { - return nil, ErrNoReply - } - reply, err = rs.replies[0].reply, rs.replies[0].err - rs.replies = rs.replies[1:] - return -} - -type cmd struct { - commandName string - args []interface{} -} - -func (p *pipeliner) Send(commandName string, args ...interface{}) { - p.cmds = append(p.cmds, &cmd{commandName: commandName, args: args}) -} - -func (p *pipeliner) Exec(ctx context.Context) (rs *Replies, err error) { - n := len(p.cmds) - if n == 0 { - return &Replies{}, nil - } - c := p.pool.Get(ctx) - defer c.Close() - for len(p.cmds) > 0 { - cmd := p.cmds[0] - p.cmds = p.cmds[1:] - if err = c.Send(cmd.commandName, cmd.args...); err != nil { - p.cmds = p.cmds[:0] - return nil, err - } - } - if err = c.Flush(); err != nil { - p.cmds = p.cmds[:0] - return nil, err - } - rps := make([]*reply, 0, n) - for i := 0; i < n; i++ { - rp, err := c.Receive() - rps = append(rps, &reply{reply: rp, err: err}) - } - rs = &Replies{ - replies: rps, - } - return -} diff --git a/pkg/cache/redis/pipeline_test.go b/pkg/cache/redis/pipeline_test.go deleted file mode 100644 index 9b5d4ee53..000000000 --- a/pkg/cache/redis/pipeline_test.go +++ /dev/null @@ -1,96 +0,0 @@ -package redis - -import ( - "context" - "fmt" - "reflect" - "testing" - "time" - - "github.com/go-kratos/kratos/pkg/container/pool" - xtime "github.com/go-kratos/kratos/pkg/time" -) - -func TestRedis_Pipeline(t *testing.T) { - conf := &Config{ - Name: "test", - Proto: "tcp", - Addr: testRedisAddr, - DialTimeout: xtime.Duration(1 * time.Second), - ReadTimeout: xtime.Duration(1 * time.Second), - WriteTimeout: xtime.Duration(1 * time.Second), - } - conf.Config = &pool.Config{ - Active: 10, - Idle: 2, - IdleTimeout: xtime.Duration(90 * time.Second), - } - - r := NewRedis(conf) - r.Do(context.TODO(), "FLUSHDB") - - p := r.Pipeline() - - for _, cmd := range testCommands { - p.Send(cmd.args[0].(string), cmd.args[1:]...) - } - - replies, err := p.Exec(context.TODO()) - - i := 0 - for replies.Next() { - cmd := testCommands[i] - actual, err := replies.Scan() - if err != nil { - t.Fatalf("Receive(%v) returned error %v", cmd.args, err) - } - if !reflect.DeepEqual(actual, cmd.expected) { - t.Errorf("Receive(%v) = %v, want %v", cmd.args, actual, cmd.expected) - } - i++ - } - err = r.Close() - if err != nil { - t.Errorf("Close() error %v", err) - } -} - -func ExamplePipeliner() { - r := NewRedis(testConfig) - defer r.Close() - - pip := r.Pipeline() - pip.Send("SET", "hello", "world") - pip.Send("GET", "hello") - replies, err := pip.Exec(context.TODO()) - if err != nil { - fmt.Printf("%#v\n", err) - } - for replies.Next() { - s, err := String(replies.Scan()) - if err != nil { - fmt.Printf("err %#v\n", err) - } - fmt.Printf("%#v\n", s) - } - // Output: - // "OK" - // "world" -} - -func BenchmarkRedisPipelineExec(b *testing.B) { - r := NewRedis(testConfig) - defer r.Close() - - r.Do(context.TODO(), "SET", "abcde", "fghiasdfasdf") - - b.ResetTimer() - for i := 0; i < b.N; i++ { - p := r.Pipeline() - p.Send("GET", "abcde") - _, err := p.Exec(context.TODO()) - if err != nil { - b.Fatal(err) - } - } -} diff --git a/pkg/cache/redis/pool.go b/pkg/cache/redis/pool.go deleted file mode 100644 index ecab41e29..000000000 --- a/pkg/cache/redis/pool.go +++ /dev/null @@ -1,240 +0,0 @@ -// Copyright 2012 Gary Burd -// -// Licensed under the Apache License, Version 2.0 (the "License"): you may -// not use this file except in compliance with the License. You may obtain -// a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -// License for the specific language governing permissions and limitations -// under the License. - -package redis - -import ( - "bytes" - "context" - "crypto/rand" - "crypto/sha1" - "errors" - "io" - "strconv" - "sync" - "time" - - "github.com/go-kratos/kratos/pkg/container/pool" - "github.com/go-kratos/kratos/pkg/net/trace" - xtime "github.com/go-kratos/kratos/pkg/time" -) - -var beginTime, _ = time.Parse("2006-01-02 15:04:05", "2006-01-02 15:04:05") - -var ( - errConnClosed = errors.New("redigo: connection closed") -) - -// Pool . -type Pool struct { - *pool.Slice - // config - c *Config - // statfunc - statfunc func(name, addr, cmd string, t time.Time, err error) func() -} - -// NewPool creates a new pool. -func NewPool(c *Config, options ...DialOption) (p *Pool) { - if c.DialTimeout <= 0 || c.ReadTimeout <= 0 || c.WriteTimeout <= 0 { - panic("must config redis timeout") - } - if c.SlowLog <= 0 { - c.SlowLog = xtime.Duration(250 * time.Millisecond) - } - ops := []DialOption{ - DialConnectTimeout(time.Duration(c.DialTimeout)), - DialReadTimeout(time.Duration(c.ReadTimeout)), - DialWriteTimeout(time.Duration(c.WriteTimeout)), - DialPassword(c.Auth), - DialDatabase(c.Db), - } - ops = append(ops, options...) - p1 := pool.NewSlice(c.Config) - - // new pool - p1.New = func(ctx context.Context) (io.Closer, error) { - conn, err := Dial(c.Proto, c.Addr, ops...) - if err != nil { - return nil, err - } - return &traceConn{ - Conn: conn, - connTags: []trace.Tag{trace.TagString(trace.TagPeerAddress, c.Addr)}, - slowLogThreshold: time.Duration(c.SlowLog), - }, nil - } - p = &Pool{Slice: p1, c: c, statfunc: pstat} - return -} - -// Get gets a connection. The application must close the returned connection. -// This method always returns a valid connection so that applications can defer -// error handling to the first use of the connection. If there is an error -// getting an underlying connection, then the connection Err, Do, Send, Flush -// and Receive methods return that error. -func (p *Pool) Get(ctx context.Context) Conn { - c, err := p.Slice.Get(ctx) - if err != nil { - return errorConnection{err} - } - c1, _ := c.(Conn) - return &pooledConnection{p: p, c: c1.WithContext(ctx), rc: c1, now: beginTime} -} - -// Close releases the resources used by the pool. -func (p *Pool) Close() error { - return p.Slice.Close() -} - -type pooledConnection struct { - p *Pool - rc Conn - c Conn - state int - - now time.Time - cmds []string -} - -var ( - sentinel []byte - sentinelOnce sync.Once -) - -func initSentinel() { - p := make([]byte, 64) - if _, err := rand.Read(p); err == nil { - sentinel = p - } else { - h := sha1.New() - io.WriteString(h, "Oops, rand failed. Use time instead.") - io.WriteString(h, strconv.FormatInt(time.Now().UnixNano(), 10)) - sentinel = h.Sum(nil) - } -} - -// SetStatFunc set stat func. -func (p *Pool) SetStatFunc(fn func(name, addr, cmd string, t time.Time, err error) func()) { - p.statfunc = fn -} - -func pstat(name, addr, cmd string, t time.Time, err error) func() { - return func() { - _metricReqDur.Observe(int64(time.Since(t)/time.Millisecond), name, addr, cmd) - if err != nil { - if msg := formatErr(err, name, addr); msg != "" { - _metricReqErr.Inc(name, addr, cmd, msg) - } - return - } - _metricHits.Inc(name, addr) - } -} - -func (pc *pooledConnection) Close() error { - c := pc.c - if _, ok := c.(errorConnection); ok { - return nil - } - pc.c = errorConnection{errConnClosed} - - if pc.state&MultiState != 0 { - c.Send("DISCARD") - pc.state &^= (MultiState | WatchState) - } else if pc.state&WatchState != 0 { - c.Send("UNWATCH") - pc.state &^= WatchState - } - if pc.state&SubscribeState != 0 { - c.Send("UNSUBSCRIBE") - c.Send("PUNSUBSCRIBE") - // To detect the end of the message stream, ask the server to echo - // a sentinel value and read until we see that value. - sentinelOnce.Do(initSentinel) - c.Send("ECHO", sentinel) - c.Flush() - for { - p, err := c.Receive() - if err != nil { - break - } - if p, ok := p.([]byte); ok && bytes.Equal(p, sentinel) { - pc.state &^= SubscribeState - break - } - } - } - _, err := c.Do("") - pc.p.Slice.Put(context.Background(), pc.rc, pc.state != 0 || c.Err() != nil) - return err -} - -func (pc *pooledConnection) Err() error { - return pc.c.Err() -} - -func (pc *pooledConnection) Do(commandName string, args ...interface{}) (reply interface{}, err error) { - now := time.Now() - ci := LookupCommandInfo(commandName) - pc.state = (pc.state | ci.Set) &^ ci.Clear - reply, err = pc.c.Do(commandName, args...) - if pc.p.statfunc != nil { - pc.p.statfunc(pc.p.c.Name, pc.p.c.Addr, commandName, now, err)() - } - return -} - -func (pc *pooledConnection) Send(commandName string, args ...interface{}) (err error) { - ci := LookupCommandInfo(commandName) - pc.state = (pc.state | ci.Set) &^ ci.Clear - if pc.now.Equal(beginTime) { - // mark first send time - pc.now = time.Now() - } - pc.cmds = append(pc.cmds, commandName) - return pc.c.Send(commandName, args...) -} - -func (pc *pooledConnection) Flush() error { - return pc.c.Flush() -} - -func (pc *pooledConnection) Receive() (reply interface{}, err error) { - reply, err = pc.c.Receive() - if len(pc.cmds) > 0 { - cmd := pc.cmds[0] - pc.cmds = pc.cmds[1:] - if pc.p.statfunc != nil { - pc.p.statfunc(pc.p.c.Name, pc.p.c.Addr, cmd, pc.now, err)() - } - } - return -} - -func (pc *pooledConnection) WithContext(ctx context.Context) Conn { - return pc -} - -type errorConnection struct{ err error } - -func (ec errorConnection) Do(string, ...interface{}) (interface{}, error) { - return nil, ec.err -} -func (ec errorConnection) Send(string, ...interface{}) error { return ec.err } -func (ec errorConnection) Err() error { return ec.err } -func (ec errorConnection) Close() error { return ec.err } -func (ec errorConnection) Flush() error { return ec.err } -func (ec errorConnection) Receive() (interface{}, error) { return nil, ec.err } -func (ec errorConnection) WithContext(context.Context) Conn { return ec } diff --git a/pkg/cache/redis/pool_test.go b/pkg/cache/redis/pool_test.go deleted file mode 100644 index 4c0bb5f01..000000000 --- a/pkg/cache/redis/pool_test.go +++ /dev/null @@ -1,539 +0,0 @@ -// Copyright 2011 Gary Burd -// -// Licensed under the Apache License, Version 2.0 (the "License"): you may -// not use this file except in compliance with the License. You may obtain -// a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -// License for the specific language governing permissions and limitations -// under the License. - -package redis - -import ( - "context" - "errors" - "io" - "reflect" - "sync" - "testing" - "time" - - "github.com/go-kratos/kratos/pkg/container/pool" -) - -type poolTestConn struct { - d *poolDialer - err error - c Conn - ctx context.Context -} - -func (c *poolTestConn) Flush() error { - return c.c.Flush() -} - -func (c *poolTestConn) Receive() (reply interface{}, err error) { - return c.c.Receive() -} - -func (c *poolTestConn) WithContext(ctx context.Context) Conn { - c.c.WithContext(ctx) - c.ctx = ctx - return c -} - -func (c *poolTestConn) Close() error { - c.d.mu.Lock() - c.d.open-- - c.d.mu.Unlock() - return c.c.Close() -} - -func (c *poolTestConn) Err() error { return c.err } - -func (c *poolTestConn) Do(commandName string, args ...interface{}) (reply interface{}, err error) { - if commandName == "ERR" { - c.err = args[0].(error) - commandName = "PING" - } - if commandName != "" { - c.d.commands = append(c.d.commands, commandName) - } - return c.c.Do(commandName, args...) -} - -func (c *poolTestConn) Send(commandName string, args ...interface{}) error { - c.d.commands = append(c.d.commands, commandName) - return c.c.Send(commandName, args...) -} - -type poolDialer struct { - mu sync.Mutex - t *testing.T - dialed int - open int - commands []string - dialErr error -} - -func (d *poolDialer) dial() (Conn, error) { - d.mu.Lock() - d.dialed += 1 - dialErr := d.dialErr - d.mu.Unlock() - if dialErr != nil { - return nil, d.dialErr - } - c, err := DialDefaultServer() - if err != nil { - return nil, err - } - d.mu.Lock() - d.open += 1 - d.mu.Unlock() - return &poolTestConn{d: d, c: c}, nil -} - -func (d *poolDialer) check(message string, p *Pool, dialed, open int) { - d.mu.Lock() - if d.dialed != dialed { - d.t.Errorf("%s: dialed=%d, want %d", message, d.dialed, dialed) - } - if d.open != open { - d.t.Errorf("%s: open=%d, want %d", message, d.open, open) - } - // if active := p.ActiveCount(); active != open { - // d.t.Errorf("%s: active=%d, want %d", message, active, open) - // } - d.mu.Unlock() -} - -func TestPoolReuse(t *testing.T) { - d := poolDialer{t: t} - p := NewPool(testConfig) - p.Slice.New = func(ctx context.Context) (io.Closer, error) { - return d.dial() - } - var err error - - for i := 0; i < 10; i++ { - c1 := p.Get(context.TODO()) - c1.Do("PING") - c2 := p.Get(context.TODO()) - c2.Do("PING") - c1.Close() - c2.Close() - } - - d.check("before close", p, 2, 2) - err = p.Close() - if err != nil { - t.Fatal(err) - } - d.check("after close", p, 2, 0) -} - -func TestPoolMaxIdle(t *testing.T) { - d := poolDialer{t: t} - p := NewPool(testConfig) - p.Slice.New = func(ctx context.Context) (io.Closer, error) { - return d.dial() - } - defer p.Close() - - for i := 0; i < 10; i++ { - c1 := p.Get(context.TODO()) - c1.Do("PING") - c2 := p.Get(context.TODO()) - c2.Do("PING") - c3 := p.Get(context.TODO()) - c3.Do("PING") - c1.Close() - c2.Close() - c3.Close() - } - d.check("before close", p, 12, 2) - p.Close() - d.check("after close", p, 12, 0) -} - -func TestPoolError(t *testing.T) { - d := poolDialer{t: t} - p := NewPool(testConfig) - p.Slice.New = func(ctx context.Context) (io.Closer, error) { - return d.dial() - } - defer p.Close() - - c := p.Get(context.TODO()) - c.Do("ERR", io.EOF) - if c.Err() == nil { - t.Errorf("expected c.Err() != nil") - } - c.Close() - - c = p.Get(context.TODO()) - c.Do("ERR", io.EOF) - c.Close() - - d.check(".", p, 2, 0) -} - -func TestPoolClose(t *testing.T) { - d := poolDialer{t: t} - p := NewPool(testConfig) - p.Slice.New = func(ctx context.Context) (io.Closer, error) { - return d.dial() - } - defer p.Close() - - c1 := p.Get(context.TODO()) - c1.Do("PING") - c2 := p.Get(context.TODO()) - c2.Do("PING") - c3 := p.Get(context.TODO()) - c3.Do("PING") - - c1.Close() - if _, err := c1.Do("PING"); err == nil { - t.Errorf("expected error after connection closed") - } - - c2.Close() - c2.Close() - - p.Close() - - d.check("after pool close", p, 3, 1) - - if _, err := c1.Do("PING"); err == nil { - t.Errorf("expected error after connection and pool closed") - } - - c3.Close() - - d.check("after conn close", p, 3, 0) - - c1 = p.Get(context.TODO()) - if _, err := c1.Do("PING"); err == nil { - t.Errorf("expected error after pool closed") - } -} - -func TestPoolConcurrenSendReceive(t *testing.T) { - p := NewPool(testConfig) - p.Slice.New = func(ctx context.Context) (io.Closer, error) { - return DialDefaultServer() - } - defer p.Close() - - c := p.Get(context.TODO()) - done := make(chan error, 1) - go func() { - _, err := c.Receive() - done <- err - }() - c.Send("PING") - c.Flush() - err := <-done - if err != nil { - t.Fatalf("Receive() returned error %v", err) - } - _, err = c.Do("") - if err != nil { - t.Fatalf("Do() returned error %v", err) - } - c.Close() -} - -func TestPoolMaxActive(t *testing.T) { - d := poolDialer{t: t} - conf := getTestConfig(testRedisAddr) - conf.Config = &pool.Config{ - Active: 2, - Idle: 2, - } - p := NewPool(conf) - p.Slice.New = func(ctx context.Context) (io.Closer, error) { - return d.dial() - } - defer p.Close() - - c1 := p.Get(context.TODO()) - c1.Do("PING") - c2 := p.Get(context.TODO()) - c2.Do("PING") - - d.check("1", p, 2, 2) - - c3 := p.Get(context.TODO()) - if _, err := c3.Do("PING"); err != pool.ErrPoolExhausted { - t.Errorf("expected pool exhausted") - } - - c3.Close() - d.check("2", p, 2, 2) - c2.Close() - d.check("3", p, 2, 2) - - c3 = p.Get(context.TODO()) - if _, err := c3.Do("PING"); err != nil { - t.Errorf("expected good channel, err=%v", err) - } - c3.Close() - - d.check("4", p, 2, 2) -} - -func TestPoolMonitorCleanup(t *testing.T) { - d := poolDialer{t: t} - p := NewPool(testConfig) - p.Slice.New = func(ctx context.Context) (io.Closer, error) { - return d.dial() - } - defer p.Close() - c := p.Get(context.TODO()) - c.Send("MONITOR") - c.Close() - - d.check("", p, 1, 0) -} - -func TestPoolPubSubCleanup(t *testing.T) { - d := poolDialer{t: t} - p := NewPool(testConfig) - p.Slice.New = func(ctx context.Context) (io.Closer, error) { - return d.dial() - } - defer p.Close() - - c := p.Get(context.TODO()) - c.Send("SUBSCRIBE", "x") - c.Close() - - want := []string{"SUBSCRIBE", "UNSUBSCRIBE", "PUNSUBSCRIBE", "ECHO"} - if !reflect.DeepEqual(d.commands, want) { - t.Errorf("got commands %v, want %v", d.commands, want) - } - d.commands = nil - - c = p.Get(context.TODO()) - c.Send("PSUBSCRIBE", "x*") - c.Close() - - want = []string{"PSUBSCRIBE", "UNSUBSCRIBE", "PUNSUBSCRIBE", "ECHO"} - if !reflect.DeepEqual(d.commands, want) { - t.Errorf("got commands %v, want %v", d.commands, want) - } - d.commands = nil -} - -func TestPoolTransactionCleanup(t *testing.T) { - d := poolDialer{t: t} - p := NewPool(testConfig) - p.Slice.New = func(ctx context.Context) (io.Closer, error) { - return d.dial() - } - defer p.Close() - - c := p.Get(context.TODO()) - c.Do("WATCH", "key") - c.Do("PING") - c.Close() - - want := []string{"WATCH", "PING", "UNWATCH"} - if !reflect.DeepEqual(d.commands, want) { - t.Errorf("got commands %v, want %v", d.commands, want) - } - d.commands = nil - - c = p.Get(context.TODO()) - c.Do("WATCH", "key") - c.Do("UNWATCH") - c.Do("PING") - c.Close() - - want = []string{"WATCH", "UNWATCH", "PING"} - if !reflect.DeepEqual(d.commands, want) { - t.Errorf("got commands %v, want %v", d.commands, want) - } - d.commands = nil - - c = p.Get(context.TODO()) - c.Do("WATCH", "key") - c.Do("MULTI") - c.Do("PING") - c.Close() - - want = []string{"WATCH", "MULTI", "PING", "DISCARD"} - if !reflect.DeepEqual(d.commands, want) { - t.Errorf("got commands %v, want %v", d.commands, want) - } - d.commands = nil - - c = p.Get(context.TODO()) - c.Do("WATCH", "key") - c.Do("MULTI") - c.Do("DISCARD") - c.Do("PING") - c.Close() - - want = []string{"WATCH", "MULTI", "DISCARD", "PING"} - if !reflect.DeepEqual(d.commands, want) { - t.Errorf("got commands %v, want %v", d.commands, want) - } - d.commands = nil - - c = p.Get(context.TODO()) - c.Do("WATCH", "key") - c.Do("MULTI") - c.Do("EXEC") - c.Do("PING") - c.Close() - - want = []string{"WATCH", "MULTI", "EXEC", "PING"} - if !reflect.DeepEqual(d.commands, want) { - t.Errorf("got commands %v, want %v", d.commands, want) - } - d.commands = nil -} - -func startGoroutines(p *Pool, cmd string, args ...interface{}) chan error { - errs := make(chan error, 10) - for i := 0; i < cap(errs); i++ { - go func() { - c := p.Get(context.TODO()) - _, err := c.Do(cmd, args...) - errs <- err - c.Close() - }() - } - - // Wait for goroutines to block. - time.Sleep(time.Second / 4) - - return errs -} - -func TestWaitPoolDialError(t *testing.T) { - testErr := errors.New("test") - d := poolDialer{t: t} - config1 := testConfig - config1.Config = &pool.Config{ - Active: 1, - Idle: 1, - Wait: true, - } - p := NewPool(config1) - p.Slice.New = func(ctx context.Context) (io.Closer, error) { - return d.dial() - } - defer p.Close() - - c := p.Get(context.TODO()) - errs := startGoroutines(p, "ERR", testErr) - d.check("before close", p, 1, 1) - - d.dialErr = errors.New("dial") - c.Close() - - nilCount := 0 - errCount := 0 - timeout := time.After(2 * time.Second) - for i := 0; i < cap(errs); i++ { - select { - case err := <-errs: - switch err { - case nil: - nilCount++ - case d.dialErr: - errCount++ - default: - t.Fatalf("expected dial error or nil, got %v", err) - } - case <-timeout: - t.Logf("Wait all the time and timeout %d", i) - return - } - } - if nilCount != 1 { - t.Errorf("expected one nil error, got %d", nilCount) - } - if errCount != cap(errs)-1 { - t.Errorf("expected %d dial erors, got %d", cap(errs)-1, errCount) - } - d.check("done", p, cap(errs), 0) -} - -func BenchmarkPoolGet(b *testing.B) { - b.StopTimer() - p := NewPool(testConfig) - c := p.Get(context.Background()) - if err := c.Err(); err != nil { - b.Fatal(err) - } - c.Close() - defer p.Close() - b.StartTimer() - for i := 0; i < b.N; i++ { - c := p.Get(context.Background()) - c.Close() - } -} - -func BenchmarkPoolGetErr(b *testing.B) { - b.StopTimer() - p := NewPool(testConfig) - c := p.Get(context.Background()) - if err := c.Err(); err != nil { - b.Fatal(err) - } - c.Close() - defer p.Close() - b.StartTimer() - for i := 0; i < b.N; i++ { - c = p.Get(context.Background()) - if err := c.Err(); err != nil { - b.Fatal(err) - } - c.Close() - } -} - -func BenchmarkPoolGetPing(b *testing.B) { - b.StopTimer() - p := NewPool(testConfig) - c := p.Get(context.Background()) - if err := c.Err(); err != nil { - b.Fatal(err) - } - c.Close() - defer p.Close() - b.StartTimer() - for i := 0; i < b.N; i++ { - c := p.Get(context.Background()) - if _, err := c.Do("PING"); err != nil { - b.Fatal(err) - } - c.Close() - } -} - -func BenchmarkPooledConn(b *testing.B) { - p := NewPool(testConfig) - defer p.Close() - for i := 0; i < b.N; i++ { - ctx := context.TODO() - c := p.Get(ctx) - c2 := c.WithContext(context.TODO()) - if _, err := c2.Do("PING"); err != nil { - b.Fatal(err) - } - c2.Close() - } -} diff --git a/pkg/cache/redis/pubsub.go b/pkg/cache/redis/pubsub.go deleted file mode 100644 index a4723f932..000000000 --- a/pkg/cache/redis/pubsub.go +++ /dev/null @@ -1,152 +0,0 @@ -// Copyright 2012 Gary Burd -// -// Licensed under the Apache License, Version 2.0 (the "License"): you may -// not use this file except in compliance with the License. You may obtain -// a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -// License for the specific language governing permissions and limitations -// under the License. - -package redis - -import ( - "errors" - - pkgerr "github.com/pkg/errors" -) - -var ( - errPubSub = errors.New("redigo: unknown pubsub notification") -) - -// Subscription represents a subscribe or unsubscribe notification. -type Subscription struct { - - // Kind is "subscribe", "unsubscribe", "psubscribe" or "punsubscribe" - Kind string - - // The channel that was changed. - Channel string - - // The current number of subscriptions for connection. - Count int -} - -// Message represents a message notification. -type Message struct { - - // The originating channel. - Channel string - - // The message data. - Data []byte -} - -// PMessage represents a pmessage notification. -type PMessage struct { - - // The matched pattern. - Pattern string - - // The originating channel. - Channel string - - // The message data. - Data []byte -} - -// Pong represents a pubsub pong notification. -type Pong struct { - Data string -} - -// PubSubConn wraps a Conn with convenience methods for subscribers. -type PubSubConn struct { - Conn Conn -} - -// Close closes the connection. -func (c PubSubConn) Close() error { - return c.Conn.Close() -} - -// Subscribe subscribes the connection to the specified channels. -func (c PubSubConn) Subscribe(channel ...interface{}) error { - c.Conn.Send("SUBSCRIBE", channel...) - return c.Conn.Flush() -} - -// PSubscribe subscribes the connection to the given patterns. -func (c PubSubConn) PSubscribe(channel ...interface{}) error { - c.Conn.Send("PSUBSCRIBE", channel...) - return c.Conn.Flush() -} - -// Unsubscribe unsubscribes the connection from the given channels, or from all -// of them if none is given. -func (c PubSubConn) Unsubscribe(channel ...interface{}) error { - c.Conn.Send("UNSUBSCRIBE", channel...) - return c.Conn.Flush() -} - -// PUnsubscribe unsubscribes the connection from the given patterns, or from all -// of them if none is given. -func (c PubSubConn) PUnsubscribe(channel ...interface{}) error { - c.Conn.Send("PUNSUBSCRIBE", channel...) - return c.Conn.Flush() -} - -// Ping sends a PING to the server with the specified data. -func (c PubSubConn) Ping(data string) error { - c.Conn.Send("PING", data) - return c.Conn.Flush() -} - -// Receive returns a pushed message as a Subscription, Message, PMessage, Pong -// or error. The return value is intended to be used directly in a type switch -// as illustrated in the PubSubConn example. -func (c PubSubConn) Receive() interface{} { - reply, err := Values(c.Conn.Receive()) - if err != nil { - return err - } - - var kind string - reply, err = Scan(reply, &kind) - if err != nil { - return err - } - - switch kind { - case "message": - var m Message - if _, err := Scan(reply, &m.Channel, &m.Data); err != nil { - return err - } - return m - case "pmessage": - var pm PMessage - if _, err := Scan(reply, &pm.Pattern, &pm.Channel, &pm.Data); err != nil { - return err - } - return pm - case "subscribe", "psubscribe", "unsubscribe", "punsubscribe": - s := Subscription{Kind: kind} - if _, err := Scan(reply, &s.Channel, &s.Count); err != nil { - return err - } - return s - case "pong": - var p Pong - if _, err := Scan(reply, &p.Data); err != nil { - return err - } - return p - } - return pkgerr.WithStack(errPubSub) -} diff --git a/pkg/cache/redis/pubsub_test.go b/pkg/cache/redis/pubsub_test.go deleted file mode 100644 index 69c66ffd8..000000000 --- a/pkg/cache/redis/pubsub_test.go +++ /dev/null @@ -1,146 +0,0 @@ -// Copyright 2012 Gary Burd -// -// Licensed under the Apache License, Version 2.0 (the "License"): you may -// not use this file except in compliance with the License. You may obtain -// a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -// License for the specific language governing permissions and limitations -// under the License. - -package redis - -import ( - "fmt" - "reflect" - "sync" - "testing" -) - -func publish(channel, value interface{}) { - c, err := dial() - if err != nil { - fmt.Println(err) - return - } - defer c.Close() - c.Do("PUBLISH", channel, value) -} - -// Applications can receive pushed messages from one goroutine and manage subscriptions from another goroutine. -func ExamplePubSubConn() { - c, err := dial() - if err != nil { - fmt.Println(err) - return - } - defer c.Close() - var wg sync.WaitGroup - wg.Add(2) - - psc := PubSubConn{Conn: c} - - // This goroutine receives and prints pushed notifications from the server. - // The goroutine exits when the connection is unsubscribed from all - // channels or there is an error. - go func() { - defer wg.Done() - for { - switch n := psc.Receive().(type) { - case Message: - fmt.Printf("Message: %s %s\n", n.Channel, n.Data) - case PMessage: - fmt.Printf("PMessage: %s %s %s\n", n.Pattern, n.Channel, n.Data) - case Subscription: - fmt.Printf("Subscription: %s %s %d\n", n.Kind, n.Channel, n.Count) - if n.Count == 0 { - return - } - case error: - fmt.Printf("error: %v\n", n) - return - } - } - }() - - // This goroutine manages subscriptions for the connection. - go func() { - defer wg.Done() - - psc.Subscribe("example") - psc.PSubscribe("p*") - - // The following function calls publish a message using another - // connection to the Redis server. - publish("example", "hello") - publish("example", "world") - publish("pexample", "foo") - publish("pexample", "bar") - - // Unsubscribe from all connections. This will cause the receiving - // goroutine to exit. - psc.Unsubscribe() - psc.PUnsubscribe() - }() - - wg.Wait() - - // Output: - // Subscription: subscribe example 1 - // Subscription: psubscribe p* 2 - // Message: example hello - // Message: example world - // PMessage: p* pexample foo - // PMessage: p* pexample bar - // Subscription: unsubscribe example 1 - // Subscription: punsubscribe p* 0 -} - -func expectPushed(t *testing.T, c PubSubConn, message string, expected interface{}) { - actual := c.Receive() - if !reflect.DeepEqual(actual, expected) { - t.Errorf("%s = %v, want %v", message, actual, expected) - } -} - -func TestPushed(t *testing.T) { - pc, err := DialDefaultServer() - if err != nil { - t.Fatalf("error connection to database, %v", err) - } - defer pc.Close() - - sc, err := DialDefaultServer() - if err != nil { - t.Fatalf("error connection to database, %v", err) - } - defer sc.Close() - - c := PubSubConn{Conn: sc} - - c.Subscribe("c1") - expectPushed(t, c, "Subscribe(c1)", Subscription{Kind: "subscribe", Channel: "c1", Count: 1}) - c.Subscribe("c2") - expectPushed(t, c, "Subscribe(c2)", Subscription{Kind: "subscribe", Channel: "c2", Count: 2}) - c.PSubscribe("p1") - expectPushed(t, c, "PSubscribe(p1)", Subscription{Kind: "psubscribe", Channel: "p1", Count: 3}) - c.PSubscribe("p2") - expectPushed(t, c, "PSubscribe(p2)", Subscription{Kind: "psubscribe", Channel: "p2", Count: 4}) - c.PUnsubscribe() - expectPushed(t, c, "Punsubscribe(p1)", Subscription{Kind: "punsubscribe", Channel: "p1", Count: 3}) - expectPushed(t, c, "Punsubscribe()", Subscription{Kind: "punsubscribe", Channel: "p2", Count: 2}) - - pc.Do("PUBLISH", "c1", "hello") - expectPushed(t, c, "PUBLISH c1 hello", Message{Channel: "c1", Data: []byte("hello")}) - - c.Ping("hello") - expectPushed(t, c, `Ping("hello")`, Pong{"hello"}) - - c.Conn.Send("PING") - c.Conn.Flush() - expectPushed(t, c, `Send("PING")`, Pong{}) -} diff --git a/pkg/cache/redis/redis.go b/pkg/cache/redis/redis.go deleted file mode 100644 index 634dc1cd9..000000000 --- a/pkg/cache/redis/redis.go +++ /dev/null @@ -1,79 +0,0 @@ -// Copyright 2012 Gary Burd -// -// Licensed under the Apache License, Version 2.0 (the "License"): you may -// not use this file except in compliance with the License. You may obtain -// a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -// License for the specific language governing permissions and limitations -// under the License. - -package redis - -import ( - "context" - - "github.com/go-kratos/kratos/pkg/container/pool" - xtime "github.com/go-kratos/kratos/pkg/time" -) - -// Error represents an error returned in a command reply. -type Error string - -func (err Error) Error() string { return string(err) } - -// Config client settings. -type Config struct { - *pool.Config - - Name string // redis name, for trace - Proto string - Addr string - Auth string - Db int - DialTimeout xtime.Duration - ReadTimeout xtime.Duration - WriteTimeout xtime.Duration - SlowLog xtime.Duration -} - -type Redis struct { - pool *Pool - conf *Config -} - -func NewRedis(c *Config, options ...DialOption) *Redis { - return &Redis{ - pool: NewPool(c, options...), - conf: c, - } -} - -// Do gets a new conn from pool, then execute Do with this conn, finally close this conn. -// ATTENTION: Don't use this method with transaction command like MULTI etc. Because every Do will close conn automatically, use r.Conn to get a raw conn for this situation. -func (r *Redis) Do(ctx context.Context, commandName string, args ...interface{}) (reply interface{}, err error) { - conn := r.pool.Get(ctx) - defer conn.Close() - reply, err = conn.Do(commandName, args...) - return -} - -// Close closes connection pool -func (r *Redis) Close() error { - return r.pool.Close() -} - -// Conn direct gets a connection -func (r *Redis) Conn(ctx context.Context) Conn { - return r.pool.Get(ctx) -} - -func (r *Redis) Pipeline() (p Pipeliner) { - return &pipeliner{ - pool: r.pool, - } -} diff --git a/pkg/cache/redis/redis_test.go b/pkg/cache/redis/redis_test.go deleted file mode 100644 index 0d2ede87b..000000000 --- a/pkg/cache/redis/redis_test.go +++ /dev/null @@ -1,323 +0,0 @@ -package redis - -import ( - "context" - "reflect" - "testing" - "time" - - "github.com/go-kratos/kratos/pkg/container/pool" - xtime "github.com/go-kratos/kratos/pkg/time" -) - -func TestRedis(t *testing.T) { - testSet(t, testPool) - testSend(t, testPool) - testGet(t, testPool) - testErr(t, testPool) - if err := testPool.Close(); err != nil { - t.Errorf("redis: close error(%v)", err) - } - conn, err := NewConn(testConfig) - if err != nil { - t.Errorf("redis: new conn error(%v)", err) - } - if err := conn.Close(); err != nil { - t.Errorf("redis: close error(%v)", err) - } -} - -func testSet(t *testing.T, p *Pool) { - var ( - key = "test" - value = "test" - conn = p.Get(context.TODO()) - ) - defer conn.Close() - if reply, err := conn.Do("set", key, value); err != nil { - t.Errorf("redis: conn.Do(SET, %s, %s) error(%v)", key, value, err) - } else { - t.Logf("redis: set status: %s", reply) - } -} - -func testSend(t *testing.T, p *Pool) { - var ( - key = "test" - value = "test" - expire = 1000 - conn = p.Get(context.TODO()) - ) - defer conn.Close() - if err := conn.Send("SET", key, value); err != nil { - t.Errorf("redis: conn.Send(SET, %s, %s) error(%v)", key, value, err) - } - if err := conn.Send("EXPIRE", key, expire); err != nil { - t.Errorf("redis: conn.Send(EXPIRE key(%s) expire(%d)) error(%v)", key, expire, err) - } - if err := conn.Flush(); err != nil { - t.Errorf("redis: conn.Flush error(%v)", err) - } - for i := 0; i < 2; i++ { - if _, err := conn.Receive(); err != nil { - t.Errorf("redis: conn.Receive error(%v)", err) - return - } - } - t.Logf("redis: set value: %s", value) -} - -func testGet(t *testing.T, p *Pool) { - var ( - key = "test" - conn = p.Get(context.TODO()) - ) - defer conn.Close() - if reply, err := conn.Do("GET", key); err != nil { - t.Errorf("redis: conn.Do(GET, %s) error(%v)", key, err) - } else { - t.Logf("redis: get value: %s", reply) - } -} - -func testErr(t *testing.T, p *Pool) { - conn := p.Get(context.TODO()) - if err := conn.Close(); err != nil { - t.Errorf("redis: close error(%v)", err) - } - if err := conn.Err(); err == nil { - t.Errorf("redis: err not nil") - } else { - t.Logf("redis: err: %v", err) - } -} - -func BenchmarkRedis(b *testing.B) { - conf := &Config{ - Name: "test", - Proto: "tcp", - Addr: testRedisAddr, - DialTimeout: xtime.Duration(time.Second), - ReadTimeout: xtime.Duration(time.Second), - WriteTimeout: xtime.Duration(time.Second), - } - conf.Config = &pool.Config{ - Active: 10, - Idle: 5, - IdleTimeout: xtime.Duration(90 * time.Second), - } - benchmarkPool := NewPool(conf) - - b.ResetTimer() - b.RunParallel(func(pb *testing.PB) { - for pb.Next() { - conn := benchmarkPool.Get(context.TODO()) - if err := conn.Close(); err != nil { - b.Errorf("redis: close error(%v)", err) - } - } - }) - if err := benchmarkPool.Close(); err != nil { - b.Errorf("redis: close error(%v)", err) - } -} - -var testRedisCommands = []struct { - args []interface{} - expected interface{} -}{ - { - []interface{}{"PING"}, - "PONG", - }, - { - []interface{}{"SET", "foo", "bar"}, - "OK", - }, - { - []interface{}{"GET", "foo"}, - []byte("bar"), - }, - { - []interface{}{"GET", "nokey"}, - nil, - }, - { - []interface{}{"MGET", "nokey", "foo"}, - []interface{}{nil, []byte("bar")}, - }, - { - []interface{}{"INCR", "mycounter"}, - int64(1), - }, - { - []interface{}{"LPUSH", "mylist", "foo"}, - int64(1), - }, - { - []interface{}{"LPUSH", "mylist", "bar"}, - int64(2), - }, - { - []interface{}{"LRANGE", "mylist", 0, -1}, - []interface{}{[]byte("bar"), []byte("foo")}, - }, -} - -func TestNewRedis(t *testing.T) { - type args struct { - c *Config - options []DialOption - } - tests := []struct { - name string - args args - wantErr bool - }{ - { - "new_redis", - args{ - testConfig, - make([]DialOption, 0), - }, - false, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - r := NewRedis(tt.args.c, tt.args.options...) - if r == nil { - t.Errorf("NewRedis() error, got nil") - return - } - err := r.Close() - if err != nil { - t.Errorf("Close() error %v", err) - } - }) - } -} - -func TestRedis_Do(t *testing.T) { - r := NewRedis(testConfig) - r.Do(context.TODO(), "FLUSHDB") - - for _, cmd := range testRedisCommands { - actual, err := r.Do(context.TODO(), cmd.args[0].(string), cmd.args[1:]...) - if err != nil { - t.Errorf("Do(%v) returned error %v", cmd.args, err) - continue - } - if !reflect.DeepEqual(actual, cmd.expected) { - t.Errorf("Do(%v) = %v, want %v", cmd.args, actual, cmd.expected) - } - } - err := r.Close() - if err != nil { - t.Errorf("Close() error %v", err) - } -} - -func TestRedis_Conn(t *testing.T) { - type args struct { - ctx context.Context - } - tests := []struct { - name string - p *Redis - args args - wantErr bool - g int - c int - }{ - { - "Close", - NewRedis(&Config{ - Config: &pool.Config{ - Active: 1, - Idle: 1, - }, - Name: "test_get", - Proto: "tcp", - Addr: testRedisAddr, - DialTimeout: xtime.Duration(time.Second), - ReadTimeout: xtime.Duration(time.Second), - WriteTimeout: xtime.Duration(time.Second), - }), - args{context.TODO()}, - false, - 3, - 3, - }, - { - "CloseExceededPoolSize", - NewRedis(&Config{ - Config: &pool.Config{ - Active: 1, - Idle: 1, - }, - Name: "test_get_out", - Proto: "tcp", - Addr: testRedisAddr, - DialTimeout: xtime.Duration(time.Second), - ReadTimeout: xtime.Duration(time.Second), - WriteTimeout: xtime.Duration(time.Second), - }), - args{context.TODO()}, - true, - 5, - 3, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - for i := 1; i <= tt.g; i++ { - got := tt.p.Conn(tt.args.ctx) - if err := got.Close(); err != nil { - if !tt.wantErr { - t.Error(err) - } - } - if i <= tt.c { - if err := got.Close(); err != nil { - t.Error(err) - } - } - } - }) - } -} - -func BenchmarkRedisDoPing(b *testing.B) { - r := NewRedis(testConfig) - defer r.Close() - b.ResetTimer() - for i := 0; i < b.N; i++ { - if _, err := r.Do(context.Background(), "PING"); err != nil { - b.Fatal(err) - } - } -} - -func BenchmarkRedisDoSET(b *testing.B) { - r := NewRedis(testConfig) - defer r.Close() - b.ResetTimer() - for i := 0; i < b.N; i++ { - if _, err := r.Do(context.Background(), "SET", "a", "b"); err != nil { - b.Fatal(err) - } - } -} - -func BenchmarkRedisDoGET(b *testing.B) { - r := NewRedis(testConfig) - defer r.Close() - r.Do(context.Background(), "SET", "a", "b") - b.ResetTimer() - for i := 0; i < b.N; i++ { - if _, err := r.Do(context.Background(), "GET", "b"); err != nil { - b.Fatal(err) - } - } -} diff --git a/pkg/cache/redis/reply.go b/pkg/cache/redis/reply.go deleted file mode 100644 index 8e4349e83..000000000 --- a/pkg/cache/redis/reply.go +++ /dev/null @@ -1,409 +0,0 @@ -// Copyright 2012 Gary Burd -// -// Licensed under the Apache License, Version 2.0 (the "License"): you may -// not use this file except in compliance with the License. You may obtain -// a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -// License for the specific language governing permissions and limitations -// under the License. - -package redis - -import ( - "errors" - "strconv" - - pkgerr "github.com/pkg/errors" -) - -// ErrNil indicates that a reply value is nil. -var ErrNil = errors.New("redigo: nil returned") - -// Int is a helper that converts a command reply to an integer. If err is not -// equal to nil, then Int returns 0, err. Otherwise, Int converts the -// reply to an int as follows: -// -// Reply type Result -// integer int(reply), nil -// bulk string parsed reply, nil -// nil 0, ErrNil -// other 0, error -func Int(reply interface{}, err error) (int, error) { - if err != nil { - return 0, err - } - switch reply := reply.(type) { - case int64: - x := int(reply) - if int64(x) != reply { - return 0, pkgerr.WithStack(strconv.ErrRange) - } - return x, nil - case []byte: - n, err := strconv.ParseInt(string(reply), 10, 0) - return int(n), pkgerr.WithStack(err) - case nil: - return 0, ErrNil - case Error: - return 0, reply - } - return 0, pkgerr.Errorf("redigo: unexpected type for Int, got type %T", reply) -} - -// Int64 is a helper that converts a command reply to 64 bit integer. If err is -// not equal to nil, then Int returns 0, err. Otherwise, Int64 converts the -// reply to an int64 as follows: -// -// Reply type Result -// integer reply, nil -// bulk string parsed reply, nil -// nil 0, ErrNil -// other 0, error -func Int64(reply interface{}, err error) (int64, error) { - if err != nil { - return 0, err - } - switch reply := reply.(type) { - case int64: - return reply, nil - case []byte: - n, err := strconv.ParseInt(string(reply), 10, 64) - return n, pkgerr.WithStack(err) - case nil: - return 0, ErrNil - case Error: - return 0, reply - } - return 0, pkgerr.Errorf("redigo: unexpected type for Int64, got type %T", reply) -} - -var errNegativeInt = errors.New("redigo: unexpected value for Uint64") - -// Uint64 is a helper that converts a command reply to 64 bit integer. If err is -// not equal to nil, then Int returns 0, err. Otherwise, Int64 converts the -// reply to an int64 as follows: -// -// Reply type Result -// integer reply, nil -// bulk string parsed reply, nil -// nil 0, ErrNil -// other 0, error -func Uint64(reply interface{}, err error) (uint64, error) { - if err != nil { - return 0, err - } - switch reply := reply.(type) { - case int64: - if reply < 0 { - return 0, pkgerr.WithStack(errNegativeInt) - } - return uint64(reply), nil - case []byte: - n, err := strconv.ParseUint(string(reply), 10, 64) - return n, err - case nil: - return 0, ErrNil - case Error: - return 0, reply - } - return 0, pkgerr.Errorf("redigo: unexpected type for Uint64, got type %T", reply) -} - -// Float64 is a helper that converts a command reply to 64 bit float. If err is -// not equal to nil, then Float64 returns 0, err. Otherwise, Float64 converts -// the reply to an int as follows: -// -// Reply type Result -// bulk string parsed reply, nil -// nil 0, ErrNil -// other 0, error -func Float64(reply interface{}, err error) (float64, error) { - if err != nil { - return 0, err - } - switch reply := reply.(type) { - case []byte: - n, err := strconv.ParseFloat(string(reply), 64) - return n, pkgerr.WithStack(err) - case nil: - return 0, ErrNil - case Error: - return 0, reply - } - return 0, pkgerr.Errorf("redigo: unexpected type for Float64, got type %T", reply) -} - -// String is a helper that converts a command reply to a string. If err is not -// equal to nil, then String returns "", err. Otherwise String converts the -// reply to a string as follows: -// -// Reply type Result -// bulk string string(reply), nil -// simple string reply, nil -// nil "", ErrNil -// other "", error -func String(reply interface{}, err error) (string, error) { - if err != nil { - return "", err - } - switch reply := reply.(type) { - case []byte: - return string(reply), nil - case string: - return reply, nil - case nil: - return "", ErrNil - case Error: - return "", reply - } - return "", pkgerr.Errorf("redigo: unexpected type for String, got type %T", reply) -} - -// Bytes is a helper that converts a command reply to a slice of bytes. If err -// is not equal to nil, then Bytes returns nil, err. Otherwise Bytes converts -// the reply to a slice of bytes as follows: -// -// Reply type Result -// bulk string reply, nil -// simple string []byte(reply), nil -// nil nil, ErrNil -// other nil, error -func Bytes(reply interface{}, err error) ([]byte, error) { - if err != nil { - return nil, err - } - switch reply := reply.(type) { - case []byte: - return reply, nil - case string: - return []byte(reply), nil - case nil: - return nil, ErrNil - case Error: - return nil, reply - } - return nil, pkgerr.Errorf("redigo: unexpected type for Bytes, got type %T", reply) -} - -// Bool is a helper that converts a command reply to a boolean. If err is not -// equal to nil, then Bool returns false, err. Otherwise Bool converts the -// reply to boolean as follows: -// -// Reply type Result -// integer value != 0, nil -// bulk string strconv.ParseBool(reply) -// nil false, ErrNil -// other false, error -func Bool(reply interface{}, err error) (bool, error) { - if err != nil { - return false, err - } - switch reply := reply.(type) { - case int64: - return reply != 0, nil - case []byte: - b, e := strconv.ParseBool(string(reply)) - return b, pkgerr.WithStack(e) - case nil: - return false, ErrNil - case Error: - return false, reply - } - return false, pkgerr.Errorf("redigo: unexpected type for Bool, got type %T", reply) -} - -// MultiBulk is a helper that converts an array command reply to a []interface{}. -// -// Deprecated: Use Values instead. -func MultiBulk(reply interface{}, err error) ([]interface{}, error) { return Values(reply, err) } - -// Values is a helper that converts an array command reply to a []interface{}. -// If err is not equal to nil, then Values returns nil, err. Otherwise, Values -// converts the reply as follows: -// -// Reply type Result -// array reply, nil -// nil nil, ErrNil -// other nil, error -func Values(reply interface{}, err error) ([]interface{}, error) { - if err != nil { - return nil, err - } - switch reply := reply.(type) { - case []interface{}: - return reply, nil - case nil: - return nil, ErrNil - case Error: - return nil, reply - } - return nil, pkgerr.Errorf("redigo: unexpected type for Values, got type %T", reply) -} - -// Strings is a helper that converts an array command reply to a []string. If -// err is not equal to nil, then Strings returns nil, err. Nil array items are -// converted to "" in the output slice. Strings returns an error if an array -// item is not a bulk string or nil. -func Strings(reply interface{}, err error) ([]string, error) { - if err != nil { - return nil, err - } - switch reply := reply.(type) { - case []interface{}: - result := make([]string, len(reply)) - for i := range reply { - if reply[i] == nil { - continue - } - p, ok := reply[i].([]byte) - if !ok { - return nil, pkgerr.Errorf("redigo: unexpected element type for Strings, got type %T", reply[i]) - } - result[i] = string(p) - } - return result, nil - case nil: - return nil, ErrNil - case Error: - return nil, reply - } - return nil, pkgerr.Errorf("redigo: unexpected type for Strings, got type %T", reply) -} - -// ByteSlices is a helper that converts an array command reply to a [][]byte. -// If err is not equal to nil, then ByteSlices returns nil, err. Nil array -// items are stay nil. ByteSlices returns an error if an array item is not a -// bulk string or nil. -func ByteSlices(reply interface{}, err error) ([][]byte, error) { - if err != nil { - return nil, err - } - switch reply := reply.(type) { - case []interface{}: - result := make([][]byte, len(reply)) - for i := range reply { - if reply[i] == nil { - continue - } - p, ok := reply[i].([]byte) - if !ok { - return nil, pkgerr.Errorf("redigo: unexpected element type for ByteSlices, got type %T", reply[i]) - } - result[i] = p - } - return result, nil - case nil: - return nil, ErrNil - case Error: - return nil, reply - } - return nil, pkgerr.Errorf("redigo: unexpected type for ByteSlices, got type %T", reply) -} - -// Ints is a helper that converts an array command reply to a []int. If -// err is not equal to nil, then Ints returns nil, err. -func Ints(reply interface{}, err error) ([]int, error) { - var ints []int - values, err := Values(reply, err) - if err != nil { - return ints, err - } - if err := ScanSlice(values, &ints); err != nil { - return ints, err - } - return ints, nil -} - -// Int64s is a helper that converts an array command reply to a []int64. If -// err is not equal to nil, then Int64s returns nil, err. -func Int64s(reply interface{}, err error) ([]int64, error) { - var int64s []int64 - values, err := Values(reply, err) - if err != nil { - return int64s, err - } - if err := ScanSlice(values, &int64s); err != nil { - return int64s, err - } - return int64s, nil -} - -// StringMap is a helper that converts an array of strings (alternating key, value) -// into a map[string]string. The HGETALL and CONFIG GET commands return replies in this format. -// Requires an even number of values in result. -func StringMap(result interface{}, err error) (map[string]string, error) { - values, err := Values(result, err) - if err != nil { - return nil, err - } - if len(values)%2 != 0 { - return nil, pkgerr.New("redigo: StringMap expects even number of values result") - } - m := make(map[string]string, len(values)/2) - for i := 0; i < len(values); i += 2 { - key, okKey := values[i].([]byte) - value, okValue := values[i+1].([]byte) - if !okKey || !okValue { - return nil, pkgerr.New("redigo: ScanMap key not a bulk string value") - } - m[string(key)] = string(value) - } - return m, nil -} - -// IntMap is a helper that converts an array of strings (alternating key, value) -// into a map[string]int. The HGETALL commands return replies in this format. -// Requires an even number of values in result. -func IntMap(result interface{}, err error) (map[string]int, error) { - values, err := Values(result, err) - if err != nil { - return nil, err - } - if len(values)%2 != 0 { - return nil, pkgerr.New("redigo: IntMap expects even number of values result") - } - m := make(map[string]int, len(values)/2) - for i := 0; i < len(values); i += 2 { - key, ok := values[i].([]byte) - if !ok { - return nil, pkgerr.New("redigo: ScanMap key not a bulk string value") - } - value, err := Int(values[i+1], nil) - if err != nil { - return nil, err - } - m[string(key)] = value - } - return m, nil -} - -// Int64Map is a helper that converts an array of strings (alternating key, value) -// into a map[string]int64. The HGETALL commands return replies in this format. -// Requires an even number of values in result. -func Int64Map(result interface{}, err error) (map[string]int64, error) { - values, err := Values(result, err) - if err != nil { - return nil, err - } - if len(values)%2 != 0 { - return nil, pkgerr.New("redigo: Int64Map expects even number of values result") - } - m := make(map[string]int64, len(values)/2) - for i := 0; i < len(values); i += 2 { - key, ok := values[i].([]byte) - if !ok { - return nil, pkgerr.New("redigo: ScanMap key not a bulk string value") - } - value, err := Int64(values[i+1], nil) - if err != nil { - return nil, err - } - m[string(key)] = value - } - return m, nil -} diff --git a/pkg/cache/redis/reply_test.go b/pkg/cache/redis/reply_test.go deleted file mode 100644 index d3b1b9551..000000000 --- a/pkg/cache/redis/reply_test.go +++ /dev/null @@ -1,179 +0,0 @@ -// Copyright 2012 Gary Burd -// -// Licensed under the Apache License, Version 2.0 (the "License"): you may -// not use this file except in compliance with the License. You may obtain -// a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -// License for the specific language governing permissions and limitations -// under the License. - -package redis - -import ( - "fmt" - "reflect" - "testing" - - "github.com/pkg/errors" -) - -type valueError struct { - v interface{} - err error -} - -func ve(v interface{}, err error) valueError { - return valueError{v, err} -} - -var replyTests = []struct { - name interface{} - actual valueError - expected valueError -}{ - { - "ints([v1, v2])", - ve(Ints([]interface{}{[]byte("4"), []byte("5")}, nil)), - ve([]int{4, 5}, nil), - }, - { - "ints(nil)", - ve(Ints(nil, nil)), - ve([]int(nil), ErrNil), - }, - { - "strings([v1, v2])", - ve(Strings([]interface{}{[]byte("v1"), []byte("v2")}, nil)), - ve([]string{"v1", "v2"}, nil), - }, - { - "strings(nil)", - ve(Strings(nil, nil)), - ve([]string(nil), ErrNil), - }, - { - "byteslices([v1, v2])", - ve(ByteSlices([]interface{}{[]byte("v1"), []byte("v2")}, nil)), - ve([][]byte{[]byte("v1"), []byte("v2")}, nil), - }, - { - "byteslices(nil)", - ve(ByteSlices(nil, nil)), - ve([][]byte(nil), ErrNil), - }, - { - "values([v1, v2])", - ve(Values([]interface{}{[]byte("v1"), []byte("v2")}, nil)), - ve([]interface{}{[]byte("v1"), []byte("v2")}, nil), - }, - { - "values(nil)", - ve(Values(nil, nil)), - ve([]interface{}(nil), ErrNil), - }, - { - "float64(1.0)", - ve(Float64([]byte("1.0"), nil)), - ve(float64(1.0), nil), - }, - { - "float64(nil)", - ve(Float64(nil, nil)), - ve(float64(0.0), ErrNil), - }, - { - "uint64(1)", - ve(Uint64(int64(1), nil)), - ve(uint64(1), nil), - }, - { - "uint64(-1)", - ve(Uint64(int64(-1), nil)), - ve(uint64(0), errNegativeInt), - }, -} - -func TestReply(t *testing.T) { - for _, rt := range replyTests { - if errors.Cause(rt.actual.err) != rt.expected.err { - t.Errorf("%s returned err %v, want %v", rt.name, rt.actual.err, rt.expected.err) - continue - } - if !reflect.DeepEqual(rt.actual.v, rt.expected.v) { - t.Errorf("%s=%+v, want %+v", rt.name, rt.actual.v, rt.expected.v) - } - } -} - -// dial wraps DialDefaultServer() with a more suitable function name for examples. -func dial() (Conn, error) { - return DialDefaultServer() -} - -func ExampleBool() { - c, err := dial() - if err != nil { - fmt.Println(err) - return - } - defer c.Close() - - c.Do("SET", "foo", 1) - exists, _ := Bool(c.Do("EXISTS", "foo")) - fmt.Printf("%#v\n", exists) - // Output: - // true -} - -func ExampleInt() { - c, err := dial() - if err != nil { - fmt.Println(err) - return - } - defer c.Close() - - c.Do("SET", "k1", 1) - n, _ := Int(c.Do("GET", "k1")) - fmt.Printf("%#v\n", n) - n, _ = Int(c.Do("INCR", "k1")) - fmt.Printf("%#v\n", n) - // Output: - // 1 - // 2 -} - -func ExampleInts() { - c, err := dial() - if err != nil { - fmt.Println(err) - return - } - defer c.Close() - - c.Do("SADD", "set_with_integers", 4, 5, 6) - ints, _ := Ints(c.Do("SMEMBERS", "set_with_integers")) - fmt.Printf("%#v\n", ints) - // Output: - // []int{4, 5, 6} -} - -func ExampleString() { - c, err := dial() - if err != nil { - fmt.Println(err) - return - } - defer c.Close() - - c.Do("SET", "hello", "world") - s, _ := String(c.Do("GET", "hello")) - fmt.Printf("%#v", s) - // Output: - // "world" -} diff --git a/pkg/cache/redis/scan.go b/pkg/cache/redis/scan.go deleted file mode 100644 index 6414a3b43..000000000 --- a/pkg/cache/redis/scan.go +++ /dev/null @@ -1,558 +0,0 @@ -// Copyright 2012 Gary Burd -// -// Licensed under the Apache License, Version 2.0 (the "License"): you may -// not use this file except in compliance with the License. You may obtain -// a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -// License for the specific language governing permissions and limitations -// under the License. - -package redis - -import ( - "errors" - "fmt" - "reflect" - "strconv" - "strings" - "sync" - - pkgerr "github.com/pkg/errors" -) - -func ensureLen(d reflect.Value, n int) { - if n > d.Cap() { - d.Set(reflect.MakeSlice(d.Type(), n, n)) - } else { - d.SetLen(n) - } -} - -func cannotConvert(d reflect.Value, s interface{}) error { - var sname string - switch s.(type) { - case string: - sname = "Redis simple string" - case Error: - sname = "Redis error" - case int64: - sname = "Redis integer" - case []byte: - sname = "Redis bulk string" - case []interface{}: - sname = "Redis array" - default: - sname = reflect.TypeOf(s).String() - } - return pkgerr.Errorf("cannot convert from %s to %s", sname, d.Type()) -} - -func convertAssignBulkString(d reflect.Value, s []byte) (err error) { - switch d.Type().Kind() { - case reflect.Float32, reflect.Float64: - var x float64 - x, err = strconv.ParseFloat(string(s), d.Type().Bits()) - d.SetFloat(x) - case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: - var x int64 - x, err = strconv.ParseInt(string(s), 10, d.Type().Bits()) - d.SetInt(x) - case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: - var x uint64 - x, err = strconv.ParseUint(string(s), 10, d.Type().Bits()) - d.SetUint(x) - case reflect.Bool: - var x bool - x, err = strconv.ParseBool(string(s)) - d.SetBool(x) - case reflect.String: - d.SetString(string(s)) - case reflect.Slice: - if d.Type().Elem().Kind() != reflect.Uint8 { - err = cannotConvert(d, s) - } else { - d.SetBytes(s) - } - default: - err = cannotConvert(d, s) - } - err = pkgerr.WithStack(err) - return -} - -func convertAssignInt(d reflect.Value, s int64) (err error) { - switch d.Type().Kind() { - case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: - d.SetInt(s) - if d.Int() != s { - err = strconv.ErrRange - d.SetInt(0) - } - case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: - if s < 0 { - err = strconv.ErrRange - } else { - x := uint64(s) - d.SetUint(x) - if d.Uint() != x { - err = strconv.ErrRange - d.SetUint(0) - } - } - case reflect.Bool: - d.SetBool(s != 0) - default: - err = cannotConvert(d, s) - } - err = pkgerr.WithStack(err) - return -} - -func convertAssignValue(d reflect.Value, s interface{}) (err error) { - switch s := s.(type) { - case []byte: - err = convertAssignBulkString(d, s) - case int64: - err = convertAssignInt(d, s) - default: - err = cannotConvert(d, s) - } - return err -} - -func convertAssignArray(d reflect.Value, s []interface{}) error { - if d.Type().Kind() != reflect.Slice { - return cannotConvert(d, s) - } - ensureLen(d, len(s)) - for i := 0; i < len(s); i++ { - if err := convertAssignValue(d.Index(i), s[i]); err != nil { - return err - } - } - return nil -} - -func convertAssign(d interface{}, s interface{}) (err error) { - // Handle the most common destination types using type switches and - // fall back to reflection for all other types. - switch s := s.(type) { - case nil: - // ingore - case []byte: - switch d := d.(type) { - case *string: - *d = string(s) - case *int: - *d, err = strconv.Atoi(string(s)) - case *bool: - *d, err = strconv.ParseBool(string(s)) - case *[]byte: - *d = s - case *interface{}: - *d = s - case nil: - // skip value - default: - if d := reflect.ValueOf(d); d.Type().Kind() != reflect.Ptr { - err = cannotConvert(d, s) - } else { - err = convertAssignBulkString(d.Elem(), s) - } - } - case int64: - switch d := d.(type) { - case *int: - x := int(s) - if int64(x) != s { - err = strconv.ErrRange - x = 0 - } - *d = x - case *bool: - *d = s != 0 - case *interface{}: - *d = s - case nil: - // skip value - default: - if d := reflect.ValueOf(d); d.Type().Kind() != reflect.Ptr { - err = cannotConvert(d, s) - } else { - err = convertAssignInt(d.Elem(), s) - } - } - case string: - switch d := d.(type) { - case *string: - *d = string(s) - default: - err = cannotConvert(reflect.ValueOf(d), s) - } - case []interface{}: - switch d := d.(type) { - case *[]interface{}: - *d = s - case *interface{}: - *d = s - case nil: - // skip value - default: - if d := reflect.ValueOf(d); d.Type().Kind() != reflect.Ptr { - err = cannotConvert(d, s) - } else { - err = convertAssignArray(d.Elem(), s) - } - } - case Error: - err = s - default: - err = cannotConvert(reflect.ValueOf(d), s) - } - err = pkgerr.WithStack(err) - return -} - -// Scan copies from src to the values pointed at by dest. -// -// The values pointed at by dest must be an integer, float, boolean, string, -// []byte, interface{} or slices of these types. Scan uses the standard strconv -// package to convert bulk strings to numeric and boolean types. -// -// If a dest value is nil, then the corresponding src value is skipped. -// -// If a src element is nil, then the corresponding dest value is not modified. -// -// To enable easy use of Scan in a loop, Scan returns the slice of src -// following the copied values. -func Scan(src []interface{}, dest ...interface{}) ([]interface{}, error) { - if len(src) < len(dest) { - return nil, pkgerr.New("redigo.Scan: array short") - } - var err error - for i, d := range dest { - err = convertAssign(d, src[i]) - if err != nil { - err = fmt.Errorf("redigo.Scan: cannot assign to dest %d: %v", i, err) - break - } - } - return src[len(dest):], err -} - -type fieldSpec struct { - name string - index []int - omitEmpty bool -} - -type structSpec struct { - m map[string]*fieldSpec - l []*fieldSpec -} - -func (ss *structSpec) fieldSpec(name []byte) *fieldSpec { - return ss.m[string(name)] -} - -func compileStructSpec(t reflect.Type, depth map[string]int, index []int, ss *structSpec) { - for i := 0; i < t.NumField(); i++ { - f := t.Field(i) - switch { - case f.PkgPath != "" && !f.Anonymous: - // Ignore unexported fields. - case f.Anonymous: - // TODO: Handle pointers. Requires change to decoder and - // protection against infinite recursion. - if f.Type.Kind() == reflect.Struct { - compileStructSpec(f.Type, depth, append(index, i), ss) - } - default: - fs := &fieldSpec{name: f.Name} - tag := f.Tag.Get("redis") - p := strings.Split(tag, ",") - if len(p) > 0 { - if p[0] == "-" { - continue - } - if len(p[0]) > 0 { - fs.name = p[0] - } - for _, s := range p[1:] { - switch s { - case "omitempty": - fs.omitEmpty = true - default: - panic(fmt.Errorf("redigo: unknown field tag %s for type %s", s, t.Name())) - } - } - } - d, found := depth[fs.name] - if !found { - d = 1 << 30 - } - switch { - case len(index) == d: - // At same depth, remove from result. - delete(ss.m, fs.name) - j := 0 - for i1 := 0; i1 < len(ss.l); i1++ { - if fs.name != ss.l[i1].name { - ss.l[j] = ss.l[i1] - j++ - } - } - ss.l = ss.l[:j] - case len(index) < d: - fs.index = make([]int, len(index)+1) - copy(fs.index, index) - fs.index[len(index)] = i - depth[fs.name] = len(index) - ss.m[fs.name] = fs - ss.l = append(ss.l, fs) - } - } - } -} - -var ( - structSpecMutex sync.RWMutex - structSpecCache = make(map[reflect.Type]*structSpec) -) - -func structSpecForType(t reflect.Type) *structSpec { - structSpecMutex.RLock() - ss, found := structSpecCache[t] - structSpecMutex.RUnlock() - if found { - return ss - } - - structSpecMutex.Lock() - defer structSpecMutex.Unlock() - ss, found = structSpecCache[t] - if found { - return ss - } - - ss = &structSpec{m: make(map[string]*fieldSpec)} - compileStructSpec(t, make(map[string]int), nil, ss) - structSpecCache[t] = ss - return ss -} - -var errScanStructValue = errors.New("redigo.ScanStruct: value must be non-nil pointer to a struct") - -// ScanStruct scans alternating names and values from src to a struct. The -// HGETALL and CONFIG GET commands return replies in this format. -// -// ScanStruct uses exported field names to match values in the response. Use -// 'redis' field tag to override the name: -// -// Field int `redis:"myName"` -// -// Fields with the tag redis:"-" are ignored. -// -// Integer, float, boolean, string and []byte fields are supported. Scan uses the -// standard strconv package to convert bulk string values to numeric and -// boolean types. -// -// If a src element is nil, then the corresponding field is not modified. -func ScanStruct(src []interface{}, dest interface{}) error { - d := reflect.ValueOf(dest) - if d.Kind() != reflect.Ptr || d.IsNil() { - return pkgerr.WithStack(errScanStructValue) - } - d = d.Elem() - if d.Kind() != reflect.Struct { - return pkgerr.WithStack(errScanStructValue) - } - ss := structSpecForType(d.Type()) - - if len(src)%2 != 0 { - return pkgerr.New("redigo.ScanStruct: number of values not a multiple of 2") - } - - for i := 0; i < len(src); i += 2 { - s := src[i+1] - if s == nil { - continue - } - name, ok := src[i].([]byte) - if !ok { - return pkgerr.Errorf("redigo.ScanStruct: key %d not a bulk string value", i) - } - fs := ss.fieldSpec(name) - if fs == nil { - continue - } - if err := convertAssignValue(d.FieldByIndex(fs.index), s); err != nil { - return pkgerr.Errorf("redigo.ScanStruct: cannot assign field %s: %v", fs.name, err) - } - } - return nil -} - -var ( - errScanSliceValue = errors.New("redigo.ScanSlice: dest must be non-nil pointer to a struct") -) - -// ScanSlice scans src to the slice pointed to by dest. The elements the dest -// slice must be integer, float, boolean, string, struct or pointer to struct -// values. -// -// Struct fields must be integer, float, boolean or string values. All struct -// fields are used unless a subset is specified using fieldNames. -func ScanSlice(src []interface{}, dest interface{}, fieldNames ...string) error { - d := reflect.ValueOf(dest) - if d.Kind() != reflect.Ptr || d.IsNil() { - return pkgerr.WithStack(errScanSliceValue) - } - d = d.Elem() - if d.Kind() != reflect.Slice { - return pkgerr.WithStack(errScanSliceValue) - } - - isPtr := false - t := d.Type().Elem() - if t.Kind() == reflect.Ptr && t.Elem().Kind() == reflect.Struct { - isPtr = true - t = t.Elem() - } - - if t.Kind() != reflect.Struct { - ensureLen(d, len(src)) - for i, s := range src { - if s == nil { - continue - } - if err := convertAssignValue(d.Index(i), s); err != nil { - return pkgerr.Errorf("redigo.ScanSlice: cannot assign element %d: %v", i, err) - } - } - return nil - } - - ss := structSpecForType(t) - fss := ss.l - if len(fieldNames) > 0 { - fss = make([]*fieldSpec, len(fieldNames)) - for i, name := range fieldNames { - fss[i] = ss.m[name] - if fss[i] == nil { - return pkgerr.Errorf("redigo.ScanSlice: ScanSlice bad field name %s", name) - } - } - } - - if len(fss) == 0 { - return pkgerr.New("redigo.ScanSlice: no struct fields") - } - - n := len(src) / len(fss) - if n*len(fss) != len(src) { - return pkgerr.New("redigo.ScanSlice: length not a multiple of struct field count") - } - - ensureLen(d, n) - for i := 0; i < n; i++ { - d1 := d.Index(i) - if isPtr { - if d1.IsNil() { - d1.Set(reflect.New(t)) - } - d1 = d1.Elem() - } - for j, fs := range fss { - s := src[i*len(fss)+j] - if s == nil { - continue - } - if err := convertAssignValue(d1.FieldByIndex(fs.index), s); err != nil { - return pkgerr.Errorf("redigo.ScanSlice: cannot assign element %d to field %s: %v", i*len(fss)+j, fs.name, err) - } - } - } - return nil -} - -// Args is a helper for constructing command arguments from structured values. -type Args []interface{} - -// Add returns the result of appending value to args. -func (args Args) Add(value ...interface{}) Args { - return append(args, value...) -} - -// AddFlat returns the result of appending the flattened value of v to args. -// -// Maps are flattened by appending the alternating keys and map values to args. -// -// Slices are flattened by appending the slice elements to args. -// -// Structs are flattened by appending the alternating names and values of -// exported fields to args. If v is a nil struct pointer, then nothing is -// appended. The 'redis' field tag overrides struct field names. See ScanStruct -// for more information on the use of the 'redis' field tag. -// -// Other types are appended to args as is. -func (args Args) AddFlat(v interface{}) Args { - rv := reflect.ValueOf(v) - switch rv.Kind() { - case reflect.Struct: - args = flattenStruct(args, rv) - case reflect.Slice: - for i := 0; i < rv.Len(); i++ { - args = append(args, rv.Index(i).Interface()) - } - case reflect.Map: - for _, k := range rv.MapKeys() { - args = append(args, k.Interface(), rv.MapIndex(k).Interface()) - } - case reflect.Ptr: - if rv.Type().Elem().Kind() == reflect.Struct { - if !rv.IsNil() { - args = flattenStruct(args, rv.Elem()) - } - } else { - args = append(args, v) - } - default: - args = append(args, v) - } - return args -} - -func flattenStruct(args Args, v reflect.Value) Args { - ss := structSpecForType(v.Type()) - for _, fs := range ss.l { - fv := v.FieldByIndex(fs.index) - if fs.omitEmpty { - var empty = false - switch fv.Kind() { - case reflect.Array, reflect.Map, reflect.Slice, reflect.String: - empty = fv.Len() == 0 - case reflect.Bool: - empty = !fv.Bool() - case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: - empty = fv.Int() == 0 - case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr: - empty = fv.Uint() == 0 - case reflect.Float32, reflect.Float64: - empty = fv.Float() == 0 - case reflect.Interface, reflect.Ptr: - empty = fv.IsNil() - } - if empty { - continue - } - } - args = append(args, fs.name, fv.Interface()) - } - return args -} diff --git a/pkg/cache/redis/scan_test.go b/pkg/cache/redis/scan_test.go deleted file mode 100644 index 119cc61d3..000000000 --- a/pkg/cache/redis/scan_test.go +++ /dev/null @@ -1,435 +0,0 @@ -// Copyright 2012 Gary Burd -// -// Licensed under the Apache License, Version 2.0 (the "License"): you may -// not use this file except in compliance with the License. You may obtain -// a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -// License for the specific language governing permissions and limitations -// under the License. - -package redis - -import ( - "fmt" - "math" - "reflect" - "testing" -) - -var scanConversionTests = []struct { - src interface{} - dest interface{} -}{ - {[]byte("-inf"), math.Inf(-1)}, - {[]byte("+inf"), math.Inf(1)}, - {[]byte("0"), float64(0)}, - {[]byte("3.14159"), float64(3.14159)}, - {[]byte("3.14"), float32(3.14)}, - {[]byte("-100"), int(-100)}, - {[]byte("101"), int(101)}, - {int64(102), int(102)}, - {[]byte("103"), uint(103)}, - {int64(104), uint(104)}, - {[]byte("105"), int8(105)}, - {int64(106), int8(106)}, - {[]byte("107"), uint8(107)}, - {int64(108), uint8(108)}, - {[]byte("0"), false}, - {int64(0), false}, - {[]byte("f"), false}, - {[]byte("1"), true}, - {int64(1), true}, - {[]byte("t"), true}, - {"hello", "hello"}, - {[]byte("hello"), "hello"}, - {[]byte("world"), []byte("world")}, - {[]interface{}{[]byte("foo")}, []interface{}{[]byte("foo")}}, - {[]interface{}{[]byte("foo")}, []string{"foo"}}, - {[]interface{}{[]byte("hello"), []byte("world")}, []string{"hello", "world"}}, - {[]interface{}{[]byte("bar")}, [][]byte{[]byte("bar")}}, - {[]interface{}{[]byte("1")}, []int{1}}, - {[]interface{}{[]byte("1"), []byte("2")}, []int{1, 2}}, - {[]interface{}{[]byte("1"), []byte("2")}, []float64{1, 2}}, - {[]interface{}{[]byte("1")}, []byte{1}}, - {[]interface{}{[]byte("1")}, []bool{true}}, -} - -func TestScanConversion(t *testing.T) { - for _, tt := range scanConversionTests { - values := []interface{}{tt.src} - dest := reflect.New(reflect.TypeOf(tt.dest)) - values, err := Scan(values, dest.Interface()) - if err != nil { - t.Errorf("Scan(%v) returned error %v", tt, err) - continue - } - if !reflect.DeepEqual(tt.dest, dest.Elem().Interface()) { - t.Errorf("Scan(%v) returned %v values: %v, want %v", tt, dest.Elem().Interface(), values, tt.dest) - } - } -} - -var scanConversionErrorTests = []struct { - src interface{} - dest interface{} -}{ - {[]byte("1234"), byte(0)}, - {int64(1234), byte(0)}, - {[]byte("-1"), byte(0)}, - {int64(-1), byte(0)}, - {[]byte("junk"), false}, - {Error("blah"), false}, -} - -func TestScanConversionError(t *testing.T) { - for _, tt := range scanConversionErrorTests { - values := []interface{}{tt.src} - dest := reflect.New(reflect.TypeOf(tt.dest)) - values, err := Scan(values, dest.Interface()) - if err == nil { - t.Errorf("Scan(%v) did not return error values: %v", tt, values) - } - } -} - -func ExampleScan() { - c, err := dial() - if err != nil { - fmt.Println(err) - return - } - defer c.Close() - - c.Send("HMSET", "album:1", "title", "Red", "rating", 5) - c.Send("HMSET", "album:2", "title", "Earthbound", "rating", 1) - c.Send("HMSET", "album:3", "title", "Beat") - c.Send("LPUSH", "albums", "1") - c.Send("LPUSH", "albums", "2") - c.Send("LPUSH", "albums", "3") - values, err := Values(c.Do("SORT", "albums", - "BY", "album:*->rating", - "GET", "album:*->title", - "GET", "album:*->rating")) - if err != nil { - fmt.Println(err) - return - } - - for len(values) > 0 { - var title string - rating := -1 // initialize to illegal value to detect nil. - values, err = Scan(values, &title, &rating) - if err != nil { - fmt.Println(err) - return - } - if rating == -1 { - fmt.Println(title, "not-rated") - } else { - fmt.Println(title, rating) - } - } - // Output: - // Beat not-rated - // Earthbound 1 - // Red 5 -} - -type s0 struct { - X int - Y int `redis:"y"` - Bt bool -} - -type s1 struct { - X int `redis:"-"` - I int `redis:"i"` - U uint `redis:"u"` - S string `redis:"s"` - P []byte `redis:"p"` - B bool `redis:"b"` - Bt bool - Bf bool - s0 -} - -var scanStructTests = []struct { - title string - reply []string - value interface{} -}{ - {"basic", - []string{"i", "-1234", "u", "5678", "s", "hello", "p", "world", "b", "t", "Bt", "1", "Bf", "0", "X", "123", "y", "456"}, - &s1{I: -1234, U: 5678, S: "hello", P: []byte("world"), B: true, Bt: true, Bf: false, s0: s0{X: 123, Y: 456}}, - }, -} - -func TestScanStruct(t *testing.T) { - for _, tt := range scanStructTests { - var reply []interface{} - for _, v := range tt.reply { - reply = append(reply, []byte(v)) - } - - value := reflect.New(reflect.ValueOf(tt.value).Type().Elem()) - - if err := ScanStruct(reply, value.Interface()); err != nil { - t.Fatalf("ScanStruct(%s) returned error %v", tt.title, err) - } - - if !reflect.DeepEqual(value.Interface(), tt.value) { - t.Fatalf("ScanStruct(%s) returned %v, want %v", tt.title, value.Interface(), tt.value) - } - } -} - -func TestBadScanStructArgs(t *testing.T) { - x := []interface{}{"A", "b"} - test := func(v interface{}) { - if err := ScanStruct(x, v); err == nil { - t.Errorf("Expect error for ScanStruct(%T, %T)", x, v) - } - } - - test(nil) - - var v0 *struct{} - test(v0) - - var v1 int - test(&v1) - - x = x[:1] - v2 := struct{ A string }{} - test(&v2) -} - -var scanSliceTests = []struct { - src []interface{} - fieldNames []string - ok bool - dest interface{} -}{ - { - []interface{}{[]byte("1"), nil, []byte("-1")}, - nil, - true, - []int{1, 0, -1}, - }, - { - []interface{}{[]byte("1"), nil, []byte("2")}, - nil, - true, - []uint{1, 0, 2}, - }, - { - []interface{}{[]byte("-1")}, - nil, - false, - []uint{1}, - }, - { - []interface{}{[]byte("hello"), nil, []byte("world")}, - nil, - true, - [][]byte{[]byte("hello"), nil, []byte("world")}, - }, - { - []interface{}{[]byte("hello"), nil, []byte("world")}, - nil, - true, - []string{"hello", "", "world"}, - }, - { - []interface{}{[]byte("a1"), []byte("b1"), []byte("a2"), []byte("b2")}, - nil, - true, - []struct{ A, B string }{{"a1", "b1"}, {"a2", "b2"}}, - }, - { - []interface{}{[]byte("a1"), []byte("b1")}, - nil, - false, - []struct{ A, B, C string }{{"a1", "b1", ""}}, - }, - { - []interface{}{[]byte("a1"), []byte("b1"), []byte("a2"), []byte("b2")}, - nil, - true, - []*struct{ A, B string }{{"a1", "b1"}, {"a2", "b2"}}, - }, - { - []interface{}{[]byte("a1"), []byte("b1"), []byte("a2"), []byte("b2")}, - []string{"A", "B"}, - true, - []struct{ A, C, B string }{{"a1", "", "b1"}, {"a2", "", "b2"}}, - }, - { - []interface{}{[]byte("a1"), []byte("b1"), []byte("a2"), []byte("b2")}, - nil, - false, - []struct{}{}, - }, -} - -func TestScanSlice(t *testing.T) { - for _, tt := range scanSliceTests { - typ := reflect.ValueOf(tt.dest).Type() - dest := reflect.New(typ) - - err := ScanSlice(tt.src, dest.Interface(), tt.fieldNames...) - if tt.ok != (err == nil) { - t.Errorf("ScanSlice(%v, []%s, %v) returned error %v", tt.src, typ, tt.fieldNames, err) - continue - } - if tt.ok && !reflect.DeepEqual(dest.Elem().Interface(), tt.dest) { - t.Errorf("ScanSlice(src, []%s) returned %#v, want %#v", typ, dest.Elem().Interface(), tt.dest) - } - } -} - -func ExampleScanSlice() { - c, err := dial() - if err != nil { - fmt.Println(err) - return - } - defer c.Close() - - c.Send("HMSET", "album:1", "title", "Red", "rating", 5) - c.Send("HMSET", "album:2", "title", "Earthbound", "rating", 1) - c.Send("HMSET", "album:3", "title", "Beat", "rating", 4) - c.Send("LPUSH", "albums", "1") - c.Send("LPUSH", "albums", "2") - c.Send("LPUSH", "albums", "3") - values, err := Values(c.Do("SORT", "albums", - "BY", "album:*->rating", - "GET", "album:*->title", - "GET", "album:*->rating")) - if err != nil { - fmt.Println(err) - return - } - - var albums []struct { - Title string - Rating int - } - if err := ScanSlice(values, &albums); err != nil { - fmt.Println(err) - return - } - fmt.Printf("%v\n", albums) - // Output: - // [{Earthbound 1} {Beat 4} {Red 5}] -} - -var argsTests = []struct { - title string - actual Args - expected Args -}{ - {"struct ptr", - Args{}.AddFlat(&struct { - I int `redis:"i"` - U uint `redis:"u"` - S string `redis:"s"` - P []byte `redis:"p"` - M map[string]string `redis:"m"` - Bt bool - Bf bool - }{ - -1234, 5678, "hello", []byte("world"), map[string]string{"hello": "world"}, true, false, - }), - Args{"i", int(-1234), "u", uint(5678), "s", "hello", "p", []byte("world"), "m", map[string]string{"hello": "world"}, "Bt", true, "Bf", false}, - }, - {"struct", - Args{}.AddFlat(struct{ I int }{123}), - Args{"I", 123}, - }, - {"slice", - Args{}.Add(1).AddFlat([]string{"a", "b", "c"}).Add(2), - Args{1, "a", "b", "c", 2}, - }, - {"struct omitempty", - Args{}.AddFlat(&struct { - I int `redis:"i,omitempty"` - U uint `redis:"u,omitempty"` - S string `redis:"s,omitempty"` - P []byte `redis:"p,omitempty"` - M map[string]string `redis:"m,omitempty"` - Bt bool `redis:"Bt,omitempty"` - Bf bool `redis:"Bf,omitempty"` - }{ - 0, 0, "", []byte{}, map[string]string{}, true, false, - }), - Args{"Bt", true}, - }, -} - -func TestArgs(t *testing.T) { - for _, tt := range argsTests { - if !reflect.DeepEqual(tt.actual, tt.expected) { - t.Fatalf("%s is %v, want %v", tt.title, tt.actual, tt.expected) - } - } -} - -func ExampleArgs() { - c, err := dial() - if err != nil { - fmt.Println(err) - return - } - defer c.Close() - - var p1, p2 struct { - Title string `redis:"title"` - Author string `redis:"author"` - Body string `redis:"body"` - } - - p1.Title = "Example" - p1.Author = "Gary" - p1.Body = "Hello" - - if _, err := c.Do("HMSET", Args{}.Add("id1").AddFlat(&p1)...); err != nil { - fmt.Println(err) - return - } - - m := map[string]string{ - "title": "Example2", - "author": "Steve", - "body": "Map", - } - - if _, err := c.Do("HMSET", Args{}.Add("id2").AddFlat(m)...); err != nil { - fmt.Println(err) - return - } - - for _, id := range []string{"id1", "id2"} { - v, err := Values(c.Do("HGETALL", id)) - if err != nil { - fmt.Println(err) - return - } - - if err := ScanStruct(v, &p2); err != nil { - fmt.Println(err) - return - } - - fmt.Printf("%+v\n", p2) - } - - // Output: - // {Title:Example Author:Gary Body:Hello} - // {Title:Example2 Author:Steve Body:Map} -} diff --git a/pkg/cache/redis/script.go b/pkg/cache/redis/script.go deleted file mode 100644 index 78605a90a..000000000 --- a/pkg/cache/redis/script.go +++ /dev/null @@ -1,86 +0,0 @@ -// Copyright 2012 Gary Burd -// -// Licensed under the Apache License, Version 2.0 (the "License"): you may -// not use this file except in compliance with the License. You may obtain -// a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -// License for the specific language governing permissions and limitations -// under the License. - -package redis - -import ( - "crypto/sha1" - "encoding/hex" - "io" - "strings" -) - -// Script encapsulates the source, hash and key count for a Lua script. See -// http://redis.io/commands/eval for information on scripts in Redis. -type Script struct { - keyCount int - src string - hash string -} - -// NewScript returns a new script object. If keyCount is greater than or equal -// to zero, then the count is automatically inserted in the EVAL command -// argument list. If keyCount is less than zero, then the application supplies -// the count as the first value in the keysAndArgs argument to the Do, Send and -// SendHash methods. -func NewScript(keyCount int, src string) *Script { - h := sha1.New() - io.WriteString(h, src) - return &Script{keyCount, src, hex.EncodeToString(h.Sum(nil))} -} - -func (s *Script) args(spec string, keysAndArgs []interface{}) []interface{} { - var args []interface{} - if s.keyCount < 0 { - args = make([]interface{}, 1+len(keysAndArgs)) - args[0] = spec - copy(args[1:], keysAndArgs) - } else { - args = make([]interface{}, 2+len(keysAndArgs)) - args[0] = spec - args[1] = s.keyCount - copy(args[2:], keysAndArgs) - } - return args -} - -// Do evaluates the script. Under the covers, Do optimistically evaluates the -// script using the EVALSHA command. If the command fails because the script is -// not loaded, then Do evaluates the script using the EVAL command (thus -// causing the script to load). -func (s *Script) Do(c Conn, keysAndArgs ...interface{}) (interface{}, error) { - v, err := c.Do("EVALSHA", s.args(s.hash, keysAndArgs)...) - if e, ok := err.(Error); ok && strings.HasPrefix(string(e), "NOSCRIPT ") { - v, err = c.Do("EVAL", s.args(s.src, keysAndArgs)...) - } - return v, err -} - -// SendHash evaluates the script without waiting for the reply. The script is -// evaluated with the EVALSHA command. The application must ensure that the -// script is loaded by a previous call to Send, Do or Load methods. -func (s *Script) SendHash(c Conn, keysAndArgs ...interface{}) error { - return c.Send("EVALSHA", s.args(s.hash, keysAndArgs)...) -} - -// Send evaluates the script without waiting for the reply. -func (s *Script) Send(c Conn, keysAndArgs ...interface{}) error { - return c.Send("EVAL", s.args(s.src, keysAndArgs)...) -} - -// Load loads the script without evaluating it. -func (s *Script) Load(c Conn) error { - _, err := c.Do("SCRIPT", "LOAD", s.src) - return err -} diff --git a/pkg/cache/redis/script_test.go b/pkg/cache/redis/script_test.go deleted file mode 100644 index 9c2c3e8a7..000000000 --- a/pkg/cache/redis/script_test.go +++ /dev/null @@ -1,102 +0,0 @@ -// Copyright 2012 Gary Burd -// -// Licensed under the Apache License, Version 2.0 (the "License"): you may -// not use this file except in compliance with the License. You may obtain -// a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -// License for the specific language governing permissions and limitations -// under the License. - -package redis - -import ( - "fmt" - "reflect" - "testing" - "time" -) - -func ExampleScript() { - c, err := Dial("tcp", ":6379") - if err != nil { - // handle error - } - defer c.Close() - // Initialize a package-level variable with a script. - var getScript = NewScript(1, `return call('get', KEYS[1])`) - - // In a function, use the script Do method to evaluate the script. The Do - // method optimistically uses the EVALSHA command. If the script is not - // loaded, then the Do method falls back to the EVAL command. - if _, err = getScript.Do(c, "foo"); err != nil { - // handle error - } -} - -func TestScript(t *testing.T) { - c, err := DialDefaultServer() - if err != nil { - t.Fatalf("error connection to database, %v", err) - } - defer c.Close() - - // To test fall back in Do, we make script unique by adding comment with current time. - script := fmt.Sprintf("--%d\nreturn {KEYS[1],KEYS[2],ARGV[1],ARGV[2]}", time.Now().UnixNano()) - s := NewScript(2, script) - reply := []interface{}{[]byte("key1"), []byte("key2"), []byte("arg1"), []byte("arg2")} - - v, err := s.Do(c, "key1", "key2", "arg1", "arg2") - if err != nil { - t.Errorf("s.Do(c, ...) returned %v", err) - } - - if !reflect.DeepEqual(v, reply) { - t.Errorf("s.Do(c, ..); = %v, want %v", v, reply) - } - - err = s.Load(c) - if err != nil { - t.Errorf("s.Load(c) returned %v", err) - } - - err = s.SendHash(c, "key1", "key2", "arg1", "arg2") - if err != nil { - t.Errorf("s.SendHash(c, ...) returned %v", err) - } - - err = c.Flush() - if err != nil { - t.Errorf("c.Flush() returned %v", err) - } - - v, err = c.Receive() - if err != nil { - t.Errorf("c.Receive() returned %v", err) - } - if !reflect.DeepEqual(v, reply) { - t.Errorf("s.SendHash(c, ..); c.Receive() = %v, want %v", v, reply) - } - - err = s.Send(c, "key1", "key2", "arg1", "arg2") - if err != nil { - t.Errorf("s.Send(c, ...) returned %v", err) - } - - err = c.Flush() - if err != nil { - t.Errorf("c.Flush() returned %v", err) - } - - v, err = c.Receive() - if err != nil { - t.Errorf("c.Receive() returned %v", err) - } - if !reflect.DeepEqual(v, reply) { - t.Errorf("s.Send(c, ..); c.Receive() = %v, want %v", v, reply) - } -} diff --git a/pkg/cache/redis/test/docker-compose.yaml b/pkg/cache/redis/test/docker-compose.yaml deleted file mode 100644 index 4bb1f4552..000000000 --- a/pkg/cache/redis/test/docker-compose.yaml +++ /dev/null @@ -1,12 +0,0 @@ -version: "3.7" - -services: - redis: - image: redis - ports: - - 6379:6379 - healthcheck: - test: ["CMD", "redis-cli","ping"] - interval: 20s - timeout: 1s - retries: 20 \ No newline at end of file diff --git a/pkg/cache/redis/trace.go b/pkg/cache/redis/trace.go deleted file mode 100644 index 656b616c0..000000000 --- a/pkg/cache/redis/trace.go +++ /dev/null @@ -1,152 +0,0 @@ -package redis - -import ( - "context" - "fmt" - "time" - - "github.com/go-kratos/kratos/pkg/log" - "github.com/go-kratos/kratos/pkg/net/trace" -) - -const ( - _traceComponentName = "library/cache/redis" - _tracePeerService = "redis" - _traceSpanKind = "client" -) - -var _internalTags = []trace.Tag{ - trace.TagString(trace.TagSpanKind, _traceSpanKind), - trace.TagString(trace.TagComponent, _traceComponentName), - trace.TagString(trace.TagPeerService, _tracePeerService), -} - -type traceConn struct { - // tr parent trace. - tr trace.Trace - // trPipe for pipeline, if trPipe != nil meaning on pipeline. - trPipe trace.Trace - - // connTag include e.g. ip,port - connTags []trace.Tag - - // origin redis conn - Conn - pending int - // TODO: split slow log from trace. - slowLogThreshold time.Duration -} - -func (t *traceConn) Do(commandName string, args ...interface{}) (reply interface{}, err error) { - statement := getStatement(commandName, args...) - defer t.slowLog(statement, time.Now()) - - // NOTE: ignored empty commandName - // current sdk will Do empty command after pipeline finished - if commandName == "" { - t.pending = 0 - t.trPipe = nil - return t.Conn.Do(commandName, args...) - } - if t.tr == nil { - return t.Conn.Do(commandName, args...) - } - tr := t.tr.Fork("", "Redis:"+commandName) - tr.SetTag(_internalTags...) - tr.SetTag(t.connTags...) - tr.SetTag(trace.TagString(trace.TagDBStatement, statement)) - reply, err = t.Conn.Do(commandName, args...) - tr.Finish(&err) - return -} - -func (t *traceConn) Send(commandName string, args ...interface{}) (err error) { - statement := getStatement(commandName, args...) - defer t.slowLog(statement, time.Now()) - t.pending++ - if t.tr == nil { - return t.Conn.Send(commandName, args...) - } - - if t.trPipe == nil { - t.trPipe = t.tr.Fork("", "Redis:Pipeline") - t.trPipe.SetTag(_internalTags...) - t.trPipe.SetTag(t.connTags...) - } - t.trPipe.SetLog( - trace.Log(trace.LogEvent, "Send"), - trace.Log("db.statement", statement), - ) - if err = t.Conn.Send(commandName, args...); err != nil { - t.trPipe.SetTag(trace.TagBool(trace.TagError, true)) - t.trPipe.SetLog( - trace.Log(trace.LogEvent, "Send Fail"), - trace.Log(trace.LogMessage, err.Error()), - ) - } - return err -} - -func (t *traceConn) Flush() error { - defer t.slowLog("Flush", time.Now()) - if t.trPipe == nil { - return t.Conn.Flush() - } - t.trPipe.SetLog(trace.Log(trace.LogEvent, "Flush")) - err := t.Conn.Flush() - if err != nil { - t.trPipe.SetTag(trace.TagBool(trace.TagError, true)) - t.trPipe.SetLog( - trace.Log(trace.LogEvent, "Flush Fail"), - trace.Log(trace.LogMessage, err.Error()), - ) - } - return err -} - -func (t *traceConn) Receive() (reply interface{}, err error) { - defer t.slowLog("Receive", time.Now()) - if t.trPipe == nil { - return t.Conn.Receive() - } - t.trPipe.SetLog(trace.Log(trace.LogEvent, "Receive")) - reply, err = t.Conn.Receive() - if err != nil { - t.trPipe.SetTag(trace.TagBool(trace.TagError, true)) - t.trPipe.SetLog( - trace.Log(trace.LogEvent, "Receive Fail"), - trace.Log(trace.LogMessage, err.Error()), - ) - } - if t.pending > 0 { - t.pending-- - } - if t.pending == 0 { - t.trPipe.Finish(nil) - t.trPipe = nil - } - return reply, err -} - -func (t *traceConn) WithContext(ctx context.Context) Conn { - t.Conn = t.Conn.WithContext(ctx) - t.tr, _ = trace.FromContext(ctx) - t.pending = 0 - t.trPipe = nil - return t -} - -func (t *traceConn) slowLog(statement string, now time.Time) { - du := time.Since(now) - if du > t.slowLogThreshold { - log.Warn("%s slow log statement: %s time: %v", _tracePeerService, statement, du) - } -} - -func getStatement(commandName string, args ...interface{}) (res string) { - res = commandName - if len(args) > 0 { - res = fmt.Sprintf("%s %v", commandName, args[0]) - } - return -} diff --git a/pkg/cache/redis/trace_test.go b/pkg/cache/redis/trace_test.go deleted file mode 100644 index 8d438b72b..000000000 --- a/pkg/cache/redis/trace_test.go +++ /dev/null @@ -1,213 +0,0 @@ -package redis - -import ( - "context" - "fmt" - "testing" - "time" - - "github.com/stretchr/testify/assert" - - "github.com/go-kratos/kratos/pkg/net/trace" -) - -const testTraceSlowLogThreshold = 250 * time.Millisecond - -type mockTrace struct { - tags []trace.Tag - logs []trace.LogField - perr *error - operationName string - finished bool -} - -func (m *mockTrace) Fork(serviceName string, operationName string) trace.Trace { - m.operationName = operationName - return m -} -func (m *mockTrace) Follow(serviceName string, operationName string) trace.Trace { - panic("not implemented") -} -func (m *mockTrace) Finish(err *error) { - m.perr = err - m.finished = true -} -func (m *mockTrace) SetTag(tags ...trace.Tag) trace.Trace { - m.tags = append(m.tags, tags...) - return m -} -func (m *mockTrace) SetLog(logs ...trace.LogField) trace.Trace { - m.logs = append(m.logs, logs...) - return m -} -func (m *mockTrace) Visit(fn func(k, v string)) {} -func (m *mockTrace) SetTitle(title string) {} -func (m *mockTrace) TraceID() string { return "" } - -type mockConn struct{} - -func (c *mockConn) Close() error { return nil } -func (c *mockConn) Err() error { return nil } -func (c *mockConn) Do(commandName string, args ...interface{}) (reply interface{}, err error) { - return nil, nil -} -func (c *mockConn) Send(commandName string, args ...interface{}) error { return nil } -func (c *mockConn) Flush() error { return nil } -func (c *mockConn) Receive() (reply interface{}, err error) { return nil, nil } -func (c *mockConn) WithContext(context.Context) Conn { return c } - -func TestTraceDo(t *testing.T) { - tr := &mockTrace{} - ctx := trace.NewContext(context.Background(), tr) - tc := &traceConn{Conn: &mockConn{}, slowLogThreshold: testTraceSlowLogThreshold} - conn := tc.WithContext(ctx) - - conn.Do("GET", "test") - - assert.Equal(t, "Redis:GET", tr.operationName) - assert.NotEmpty(t, tr.tags) - assert.True(t, tr.finished) -} - -func TestTraceDoErr(t *testing.T) { - tr := &mockTrace{} - ctx := trace.NewContext(context.Background(), tr) - tc := &traceConn{Conn: MockErr{Error: fmt.Errorf("hhhhhhh")}, - slowLogThreshold: testTraceSlowLogThreshold} - conn := tc.WithContext(ctx) - - conn.Do("GET", "test") - - assert.Equal(t, "Redis:GET", tr.operationName) - assert.True(t, tr.finished) - assert.NotNil(t, *tr.perr) -} - -func TestTracePipeline(t *testing.T) { - tr := &mockTrace{} - ctx := trace.NewContext(context.Background(), tr) - tc := &traceConn{Conn: &mockConn{}, slowLogThreshold: testTraceSlowLogThreshold} - conn := tc.WithContext(ctx) - - N := 2 - for i := 0; i < N; i++ { - conn.Send("GET", "hello, world") - } - conn.Flush() - for i := 0; i < N; i++ { - conn.Receive() - } - - assert.Equal(t, "Redis:Pipeline", tr.operationName) - assert.NotEmpty(t, tr.tags) - assert.NotEmpty(t, tr.logs) - assert.True(t, tr.finished) -} - -func TestTracePipelineErr(t *testing.T) { - tr := &mockTrace{} - ctx := trace.NewContext(context.Background(), tr) - tc := &traceConn{Conn: MockErr{Error: fmt.Errorf("hahah")}, - slowLogThreshold: testTraceSlowLogThreshold} - conn := tc.WithContext(ctx) - - N := 2 - for i := 0; i < N; i++ { - conn.Send("GET", "hello, world") - } - conn.Flush() - for i := 0; i < N; i++ { - conn.Receive() - } - - assert.Equal(t, "Redis:Pipeline", tr.operationName) - assert.NotEmpty(t, tr.tags) - assert.NotEmpty(t, tr.logs) - assert.True(t, tr.finished) - var isError bool - for _, tag := range tr.tags { - if tag.Key == "error" { - isError = true - } - } - assert.True(t, isError) -} - -func TestSendStatement(t *testing.T) { - tr := &mockTrace{} - ctx := trace.NewContext(context.Background(), tr) - tc := &traceConn{Conn: MockErr{Error: fmt.Errorf("hahah")}, - slowLogThreshold: testTraceSlowLogThreshold} - conn := tc.WithContext(ctx) - conn.Send("SET", "hello", "test") - conn.Flush() - conn.Receive() - - assert.Equal(t, "Redis:Pipeline", tr.operationName) - assert.NotEmpty(t, tr.tags) - assert.NotEmpty(t, tr.logs) - assert.Equal(t, "event", tr.logs[0].Key) - assert.Equal(t, "Send", tr.logs[0].Value) - assert.Equal(t, "db.statement", tr.logs[1].Key) - assert.Equal(t, "SET hello", tr.logs[1].Value) - assert.True(t, tr.finished) - var isError bool - for _, tag := range tr.tags { - if tag.Key == "error" { - isError = true - } - } - assert.True(t, isError) -} - -func TestDoStatement(t *testing.T) { - tr := &mockTrace{} - ctx := trace.NewContext(context.Background(), tr) - tc := &traceConn{Conn: MockErr{Error: fmt.Errorf("hahah")}, - slowLogThreshold: testTraceSlowLogThreshold} - conn := tc.WithContext(ctx) - conn.Do("SET", "hello", "test") - - assert.Equal(t, "Redis:SET", tr.operationName) - assert.Equal(t, "SET hello", tr.tags[len(tr.tags)-1].Value) - assert.True(t, tr.finished) -} - -func BenchmarkTraceConn(b *testing.B) { - for i := 0; i < b.N; i++ { - c, err := DialDefaultServer() - if err != nil { - b.Fatal(err) - } - t := &traceConn{ - Conn: c, - connTags: []trace.Tag{trace.TagString(trace.TagPeerAddress, "abc")}, - slowLogThreshold: 1 * time.Second, - } - c2 := t.WithContext(context.TODO()) - if _, err := c2.Do("PING"); err != nil { - b.Fatal(err) - } - c2.Close() - } -} - -func TestTraceConnPending(t *testing.T) { - c, err := DialDefaultServer() - if err != nil { - t.Fatal(err) - } - tc := &traceConn{ - Conn: c, - connTags: []trace.Tag{trace.TagString(trace.TagPeerAddress, "abc")}, - slowLogThreshold: 1 * time.Second, - } - err = tc.Send("SET", "a", "x") - if err != nil { - t.Fatal(err) - } - tc.Close() - assert.Equal(t, 1, tc.pending) - tc.Do("") - assert.Equal(t, 0, tc.pending) -} diff --git a/pkg/cache/redis/util.go b/pkg/cache/redis/util.go deleted file mode 100644 index aa52597bb..000000000 --- a/pkg/cache/redis/util.go +++ /dev/null @@ -1,17 +0,0 @@ -package redis - -import ( - "context" - "time" -) - -func shrinkDeadline(ctx context.Context, timeout time.Duration) time.Time { - var timeoutTime = time.Now().Add(timeout) - if ctx == nil { - return timeoutTime - } - if deadline, ok := ctx.Deadline(); ok && timeoutTime.After(deadline) { - return deadline - } - return timeoutTime -} diff --git a/pkg/cache/redis/util_test.go b/pkg/cache/redis/util_test.go deleted file mode 100644 index 748b8423e..000000000 --- a/pkg/cache/redis/util_test.go +++ /dev/null @@ -1,37 +0,0 @@ -package redis - -import ( - "context" - "testing" - "time" - - "github.com/stretchr/testify/assert" -) - -func TestShrinkDeadline(t *testing.T) { - t.Run("test not deadline", func(t *testing.T) { - timeout := time.Second - timeoutTime := time.Now().Add(timeout) - tm := shrinkDeadline(context.Background(), timeout) - assert.True(t, tm.After(timeoutTime)) - }) - t.Run("test big deadline", func(t *testing.T) { - timeout := time.Second - timeoutTime := time.Now().Add(timeout) - deadlineTime := time.Now().Add(2 * time.Second) - ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second) - defer cancel() - - tm := shrinkDeadline(ctx, timeout) - assert.True(t, tm.After(timeoutTime) && tm.Before(deadlineTime)) - }) - t.Run("test small deadline", func(t *testing.T) { - timeout := time.Second - deadlineTime := time.Now().Add(500 * time.Millisecond) - ctx, cancel := context.WithTimeout(context.Background(), 500*time.Millisecond) - defer cancel() - - tm := shrinkDeadline(ctx, timeout) - assert.True(t, tm.After(deadlineTime) && tm.Before(time.Now().Add(timeout))) - }) -} diff --git a/pkg/conf/dsn/README.md b/pkg/conf/dsn/README.md deleted file mode 100644 index 57b23b585..000000000 --- a/pkg/conf/dsn/README.md +++ /dev/null @@ -1,5 +0,0 @@ -# dsn - -## 项目简介 - -通用数据源地址解析 diff --git a/pkg/conf/dsn/doc.go b/pkg/conf/dsn/doc.go deleted file mode 100644 index d93013d87..000000000 --- a/pkg/conf/dsn/doc.go +++ /dev/null @@ -1,63 +0,0 @@ -// Package dsn implements dsn parse with struct bind -/* -DSN 格式类似 URI, DSN 结构如下图 - - network:[//[username[:password]@]address[:port][,address[:port]]][/path][?query][#fragment] - -与 URI 的主要区别在于 scheme 被替换为 network, host 被替换为 address 并且支持多个 address. -network 与 net 包中 network 意义相同, tcp、udp、unix 等, address 支持多个使用 ',' 分割, 如果 -network 为 unix 等本地 sock 协议则使用 Path, 有且只有一个 - -dsn 包主要提供了 Parse, Bind 和 validate 功能 - -Parse 解析 dsn 字符串成 DSN struct, DSN struct 与 url.URL 几乎完全一样 - -Bind 提供将 DSN 数据绑定到一个 struct 的功能, 通过 tag dsn:"key,[default]" 指定绑定的字段, 目前支持两种类型的数据绑定 - -内置变量 key: - network string tcp, udp, unix 等, 参考 net 包中的 network - username string - password string - address string or []string address 可以绑定到 string 或者 []string, 如果为 string 则取 address 第一个 - -Query: 通过 query.name 可以取到 query 上的数据 - - 数组可以通过传递多个获得 - - array=1&array=2&array3 -> []int `tag:"query.array"` - - struct 支持嵌套 - - foo.sub.name=hello&foo.tm=hello - - struct Foo { - Tm string `dsn:"query.tm"` - Sub struct { - Name string `dsn:"query.name"` - } `dsn:"query.sub"` - } - -默认值: 通过 dsn:"key,[default]" 默认值暂时不支持数组 - -忽略 Bind: 通过 dsn:"-" 忽略 Bind - -自定义 Bind: 可以同时实现 encoding.TextUnmarshaler 自定义 Bind 实现 - -Validate: 参考 https://github.com/go-playground/validator - -使用参考: example_test.go - -DSN 命名规范: - -没有历史遗留的情况下,尽量使用 Address, Network, Username, Password 等命名,代替之前的 Proto 和 Addr 等命名 - -Query 命名参考, 使用驼峰小写开头: - - timeout 通用超时 - dialTimeout 连接建立超时 - readTimeout 读操作超时 - writeTimeout 写操作超时 - readsTimeout 批量读超时 - writesTimeout 批量写超时 -*/ -package dsn diff --git a/pkg/conf/dsn/dsn.go b/pkg/conf/dsn/dsn.go deleted file mode 100644 index 7cb209e2b..000000000 --- a/pkg/conf/dsn/dsn.go +++ /dev/null @@ -1,106 +0,0 @@ -package dsn - -import ( - "net/url" - "reflect" - "strings" - - validator "gopkg.in/go-playground/validator.v9" -) - -var _validator *validator.Validate - -func init() { - _validator = validator.New() -} - -// DSN a DSN represents a parsed DSN as same as url.URL. -type DSN struct { - *url.URL -} - -// Bind dsn to specify struct and validate use use go-playground/validator format -// -// The bind of each struct field can be customized by the format string -// stored under the 'dsn' key in the struct field's tag. The format string -// gives the name of the field, possibly followed by a comma-separated -// list of options. The name may be empty in order to specify options -// without overriding the default field name. -// -// A two type data you can bind to struct -// built-in values, use below keys to bind built-in value -// username -// password -// address -// network -// the value in query string, use query.{name} to bind value in query string -// -// As a special case, if the field tag is "-", the field is always omitted. -// NOTE: that a field with name "-" can still be generated using the tag "-,". -// -// Examples of struct field tags and their meanings: -// // Field bind username -// Field string `dsn:"username"` -// // Field is ignored by this package. -// Field string `dsn:"-"` -// // Field bind value from query -// Field string `dsn:"query.name"` -// -func (d *DSN) Bind(v interface{}) (url.Values, error) { - assignFuncs := make(map[string]assignFunc) - if d.User != nil { - username := d.User.Username() - password, ok := d.User.Password() - if ok { - assignFuncs["password"] = stringsAssignFunc(password) - } - assignFuncs["username"] = stringsAssignFunc(username) - } - assignFuncs["address"] = addressesAssignFunc(d.Addresses()) - assignFuncs["network"] = stringsAssignFunc(d.Scheme) - query, err := bindQuery(d.Query(), v, assignFuncs) - if err != nil { - return nil, err - } - return query, _validator.Struct(v) -} - -func addressesAssignFunc(addresses []string) assignFunc { - return func(v reflect.Value, to tagOpt) error { - if v.Kind() == reflect.String { - if addresses[0] == "" && to.Default != "" { - v.SetString(to.Default) - } else { - v.SetString(addresses[0]) - } - return nil - } - if !(v.Kind() == reflect.Slice && v.Type().Elem().Kind() == reflect.String) { - return &BindTypeError{Value: strings.Join(addresses, ","), Type: v.Type()} - } - vals := reflect.MakeSlice(v.Type(), len(addresses), len(addresses)) - for i, address := range addresses { - vals.Index(i).SetString(address) - } - if v.CanSet() { - v.Set(vals) - } - return nil - } -} - -// Addresses parse host split by ',' -// For Unix networks, return ['path'] -func (d *DSN) Addresses() []string { - switch d.Scheme { - case "unix", "unixgram", "unixpacket": - return []string{d.Path} - } - return strings.Split(d.Host, ",") -} - -// Parse parses rawdsn into a URL structure. -func Parse(rawdsn string) (*DSN, error) { - u, err := url.Parse(rawdsn) - return &DSN{URL: u}, err -} diff --git a/pkg/conf/dsn/dsn_test.go b/pkg/conf/dsn/dsn_test.go deleted file mode 100644 index 2a25acfb5..000000000 --- a/pkg/conf/dsn/dsn_test.go +++ /dev/null @@ -1,79 +0,0 @@ -package dsn - -import ( - "net/url" - "reflect" - "testing" - "time" - - xtime "github.com/go-kratos/kratos/pkg/time" -) - -type config struct { - Network string `dsn:"network"` - Addresses []string `dsn:"address"` - Username string `dsn:"username"` - Password string `dsn:"password"` - Timeout xtime.Duration `dsn:"query.timeout"` - Sub Sub `dsn:"query.sub"` - Def string `dsn:"query.def,hello"` -} - -type Sub struct { - Foo int `dsn:"query.foo"` -} - -func TestBind(t *testing.T) { - var cfg config - rawdsn := "tcp://root:toor@172.12.23.34,178.23.34.45?timeout=1s&sub.foo=1&hello=world" - dsn, err := Parse(rawdsn) - if err != nil { - t.Fatal(err) - } - values, err := dsn.Bind(&cfg) - if err != nil { - t.Error(err) - } - if !reflect.DeepEqual(values, url.Values{"hello": {"world"}}) { - t.Errorf("unexpect values get %v", values) - } - cfg2 := config{ - Network: "tcp", - Addresses: []string{"172.12.23.34", "178.23.34.45"}, - Password: "toor", - Username: "root", - Sub: Sub{Foo: 1}, - Timeout: xtime.Duration(time.Second), - Def: "hello", - } - if !reflect.DeepEqual(cfg, cfg2) { - t.Errorf("unexpect config get %v, expect %v", cfg, cfg2) - } -} - -type config2 struct { - Network string `dsn:"network"` - Address string `dsn:"address"` - Timeout xtime.Duration `dsn:"query.timeout"` -} - -func TestUnix(t *testing.T) { - var cfg config2 - rawdsn := "unix:///run/xxx.sock?timeout=1s&sub.foo=1&hello=world" - dsn, err := Parse(rawdsn) - if err != nil { - t.Fatal(err) - } - _, err = dsn.Bind(&cfg) - if err != nil { - t.Error(err) - } - cfg2 := config2{ - Network: "unix", - Address: "/run/xxx.sock", - Timeout: xtime.Duration(time.Second), - } - if !reflect.DeepEqual(cfg, cfg2) { - t.Errorf("unexpect config2 get %v, expect %v", cfg, cfg2) - } -} diff --git a/pkg/conf/dsn/example_test.go b/pkg/conf/dsn/example_test.go deleted file mode 100644 index b2ac38421..000000000 --- a/pkg/conf/dsn/example_test.go +++ /dev/null @@ -1,31 +0,0 @@ -package dsn_test - -import ( - "log" - - "github.com/go-kratos/kratos/pkg/conf/dsn" - xtime "github.com/go-kratos/kratos/pkg/time" -) - -// Config struct -type Config struct { - Network string `dsn:"network" validate:"required"` - Host string `dsn:"host" validate:"required"` - Username string `dsn:"username" validate:"required"` - Password string `dsn:"password" validate:"required"` - Timeout xtime.Duration `dsn:"query.timeout,1s"` - Offset int `dsn:"query.offset" validate:"gte=0"` -} - -func ExampleParse() { - cfg := &Config{} - d, err := dsn.Parse("tcp://root:toor@172.12.12.23:2233?timeout=10s") - if err != nil { - log.Fatal(err) - } - _, err = d.Bind(cfg) - if err != nil { - log.Fatal(err) - } - log.Printf("%v", cfg) -} diff --git a/pkg/conf/dsn/query.go b/pkg/conf/dsn/query.go deleted file mode 100644 index f622d0b07..000000000 --- a/pkg/conf/dsn/query.go +++ /dev/null @@ -1,422 +0,0 @@ -package dsn - -import ( - "encoding" - "net/url" - "reflect" - "runtime" - "strconv" - "strings" -) - -const ( - _tagID = "dsn" - _queryPrefix = "query." -) - -// InvalidBindError describes an invalid argument passed to DecodeQuery. -// (The argument to DecodeQuery must be a non-nil pointer.) -type InvalidBindError struct { - Type reflect.Type -} - -func (e *InvalidBindError) Error() string { - if e.Type == nil { - return "Bind(nil)" - } - - if e.Type.Kind() != reflect.Ptr { - return "Bind(non-pointer " + e.Type.String() + ")" - } - return "Bind(nil " + e.Type.String() + ")" -} - -// BindTypeError describes a query value that was -// not appropriate for a value of a specific Go type. -type BindTypeError struct { - Value string - Type reflect.Type -} - -func (e *BindTypeError) Error() string { - return "cannot decode " + e.Value + " into Go value of type " + e.Type.String() -} - -type assignFunc func(v reflect.Value, to tagOpt) error - -func stringsAssignFunc(val string) assignFunc { - return func(v reflect.Value, to tagOpt) error { - if v.Kind() != reflect.String || !v.CanSet() { - return &BindTypeError{Value: "string", Type: v.Type()} - } - if val == "" { - v.SetString(to.Default) - } else { - v.SetString(val) - } - return nil - } -} - -// bindQuery parses url.Values and stores the result in the value pointed to by v. -// if v is nil or not a pointer, bindQuery returns an InvalidDecodeError -func bindQuery(query url.Values, v interface{}, assignFuncs map[string]assignFunc) (url.Values, error) { - if assignFuncs == nil { - assignFuncs = make(map[string]assignFunc) - } - d := decodeState{ - data: query, - used: make(map[string]bool), - assignFuncs: assignFuncs, - } - err := d.decode(v) - ret := d.unused() - return ret, err -} - -type tagOpt struct { - Name string - Default string -} - -func parseTag(tag string) tagOpt { - vs := strings.SplitN(tag, ",", 2) - if len(vs) == 2 { - return tagOpt{Name: vs[0], Default: vs[1]} - } - return tagOpt{Name: vs[0]} -} - -type decodeState struct { - data url.Values - used map[string]bool - assignFuncs map[string]assignFunc -} - -func (d *decodeState) unused() url.Values { - ret := make(url.Values) - for k, v := range d.data { - if !d.used[k] { - ret[k] = v - } - } - return ret -} - -func (d *decodeState) decode(v interface{}) (err error) { - defer func() { - if r := recover(); r != nil { - if _, ok := r.(runtime.Error); ok { - panic(r) - } - err = r.(error) - } - }() - rv := reflect.ValueOf(v) - if rv.Kind() != reflect.Ptr || rv.IsNil() { - return &InvalidBindError{reflect.TypeOf(v)} - } - return d.root(rv) -} - -func (d *decodeState) root(v reflect.Value) error { - var tu encoding.TextUnmarshaler - tu, v = d.indirect(v) - if tu != nil { - return tu.UnmarshalText([]byte(d.data.Encode())) - } - // TODO support map, slice as root - if v.Kind() != reflect.Struct { - return &BindTypeError{Value: d.data.Encode(), Type: v.Type()} - } - tv := v.Type() - for i := 0; i < tv.NumField(); i++ { - fv := v.Field(i) - field := tv.Field(i) - to := parseTag(field.Tag.Get(_tagID)) - if to.Name == "-" { - continue - } - if af, ok := d.assignFuncs[to.Name]; ok { - if err := af(fv, tagOpt{}); err != nil { - return err - } - continue - } - if !strings.HasPrefix(to.Name, _queryPrefix) { - continue - } - to.Name = to.Name[len(_queryPrefix):] - if err := d.value(fv, "", to); err != nil { - return err - } - } - return nil -} - -func combinekey(prefix string, to tagOpt) string { - key := to.Name - if prefix != "" { - key = prefix + "." + key - } - return key -} - -func (d *decodeState) value(v reflect.Value, prefix string, to tagOpt) (err error) { - key := combinekey(prefix, to) - d.used[key] = true - var tu encoding.TextUnmarshaler - tu, v = d.indirect(v) - if tu != nil { - if val, ok := d.data[key]; ok { - return tu.UnmarshalText([]byte(val[0])) - } - if to.Default != "" { - return tu.UnmarshalText([]byte(to.Default)) - } - return - } - switch v.Kind() { - case reflect.Bool: - err = d.valueBool(v, prefix, to) - case reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64, reflect.Int: - err = d.valueInt64(v, prefix, to) - case reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uint: - err = d.valueUint64(v, prefix, to) - case reflect.Float32, reflect.Float64: - err = d.valueFloat64(v, prefix, to) - case reflect.String: - err = d.valueString(v, prefix, to) - case reflect.Slice: - err = d.valueSlice(v, prefix, to) - case reflect.Struct: - err = d.valueStruct(v, prefix, to) - case reflect.Ptr: - if !d.hasKey(combinekey(prefix, to)) { - break - } - if !v.CanSet() { - break - } - nv := reflect.New(v.Type().Elem()) - v.Set(nv) - err = d.value(nv, prefix, to) - } - return -} - -func (d *decodeState) hasKey(key string) bool { - for k := range d.data { - if strings.HasPrefix(k, key+".") || k == key { - return true - } - } - return false -} - -func (d *decodeState) valueBool(v reflect.Value, prefix string, to tagOpt) error { - key := combinekey(prefix, to) - val := d.data.Get(key) - if val == "" { - if to.Default == "" { - return nil - } - val = to.Default - } - return d.setBool(v, val) -} - -func (d *decodeState) setBool(v reflect.Value, val string) error { - bval, err := strconv.ParseBool(val) - if err != nil { - return &BindTypeError{Value: val, Type: v.Type()} - } - v.SetBool(bval) - return nil -} - -func (d *decodeState) valueInt64(v reflect.Value, prefix string, to tagOpt) error { - key := combinekey(prefix, to) - val := d.data.Get(key) - if val == "" { - if to.Default == "" { - return nil - } - val = to.Default - } - return d.setInt64(v, val) -} - -func (d *decodeState) setInt64(v reflect.Value, val string) error { - ival, err := strconv.ParseInt(val, 10, 64) - if err != nil { - return &BindTypeError{Value: val, Type: v.Type()} - } - v.SetInt(ival) - return nil -} - -func (d *decodeState) valueUint64(v reflect.Value, prefix string, to tagOpt) error { - key := combinekey(prefix, to) - val := d.data.Get(key) - if val == "" { - if to.Default == "" { - return nil - } - val = to.Default - } - return d.setUint64(v, val) -} - -func (d *decodeState) setUint64(v reflect.Value, val string) error { - uival, err := strconv.ParseUint(val, 10, 64) - if err != nil { - return &BindTypeError{Value: val, Type: v.Type()} - } - v.SetUint(uival) - return nil -} - -func (d *decodeState) valueFloat64(v reflect.Value, prefix string, to tagOpt) error { - key := combinekey(prefix, to) - val := d.data.Get(key) - if val == "" { - if to.Default == "" { - return nil - } - val = to.Default - } - return d.setFloat64(v, val) -} - -func (d *decodeState) setFloat64(v reflect.Value, val string) error { - fval, err := strconv.ParseFloat(val, 64) - if err != nil { - return &BindTypeError{Value: val, Type: v.Type()} - } - v.SetFloat(fval) - return nil -} - -func (d *decodeState) valueString(v reflect.Value, prefix string, to tagOpt) error { - key := combinekey(prefix, to) - val := d.data.Get(key) - if val == "" { - if to.Default == "" { - return nil - } - val = to.Default - } - return d.setString(v, val) -} - -func (d *decodeState) setString(v reflect.Value, val string) error { - v.SetString(val) - return nil -} - -func (d *decodeState) valueSlice(v reflect.Value, prefix string, to tagOpt) error { - key := combinekey(prefix, to) - strs, ok := d.data[key] - if !ok { - strs = strings.Split(to.Default, ",") - } - if len(strs) == 0 { - return nil - } - et := v.Type().Elem() - var setFunc func(reflect.Value, string) error - switch et.Kind() { - case reflect.Bool: - setFunc = d.setBool - case reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64, reflect.Int: - setFunc = d.setInt64 - case reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uint: - setFunc = d.setUint64 - case reflect.Float32, reflect.Float64: - setFunc = d.setFloat64 - case reflect.String: - setFunc = d.setString - default: - return &BindTypeError{Type: et, Value: strs[0]} - } - vals := reflect.MakeSlice(v.Type(), len(strs), len(strs)) - for i, str := range strs { - if err := setFunc(vals.Index(i), str); err != nil { - return err - } - } - if v.CanSet() { - v.Set(vals) - } - return nil -} - -func (d *decodeState) valueStruct(v reflect.Value, prefix string, to tagOpt) error { - tv := v.Type() - for i := 0; i < tv.NumField(); i++ { - fv := v.Field(i) - field := tv.Field(i) - fto := parseTag(field.Tag.Get(_tagID)) - if fto.Name == "-" { - continue - } - if af, ok := d.assignFuncs[fto.Name]; ok { - if err := af(fv, tagOpt{}); err != nil { - return err - } - continue - } - if !strings.HasPrefix(fto.Name, _queryPrefix) { - continue - } - fto.Name = fto.Name[len(_queryPrefix):] - if err := d.value(fv, to.Name, fto); err != nil { - return err - } - } - return nil -} - -func (d *decodeState) indirect(v reflect.Value) (encoding.TextUnmarshaler, reflect.Value) { - v0 := v - haveAddr := false - - if v.Kind() != reflect.Ptr && v.Type().Name() != "" && v.CanAddr() { - haveAddr = true - v = v.Addr() - } - for { - if v.Kind() == reflect.Interface && !v.IsNil() { - e := v.Elem() - if e.Kind() == reflect.Ptr && !e.IsNil() && e.Elem().Kind() == reflect.Ptr { - haveAddr = false - v = e - continue - } - } - - if v.Kind() != reflect.Ptr { - break - } - - if v.Elem().Kind() != reflect.Ptr && v.CanSet() { - break - } - if v.IsNil() { - v.Set(reflect.New(v.Type().Elem())) - } - if v.Type().NumMethod() > 0 { - if u, ok := v.Interface().(encoding.TextUnmarshaler); ok { - return u, reflect.Value{} - } - } - if haveAddr { - v = v0 - haveAddr = false - } else { - v = v.Elem() - } - } - return nil, v -} diff --git a/pkg/conf/dsn/query_test.go b/pkg/conf/dsn/query_test.go deleted file mode 100644 index 10d431db5..000000000 --- a/pkg/conf/dsn/query_test.go +++ /dev/null @@ -1,128 +0,0 @@ -package dsn - -import ( - "net/url" - "reflect" - "testing" - "time" - - xtime "github.com/go-kratos/kratos/pkg/time" -) - -type cfg1 struct { - Name string `dsn:"query.name"` - Def string `dsn:"query.def,hello"` - DefSlice []int `dsn:"query.defslice,1,2,3,4"` - Ignore string `dsn:"-"` - FloatNum float64 `dsn:"query.floatNum"` -} - -type cfg2 struct { - Timeout xtime.Duration `dsn:"query.timeout"` -} - -type cfg3 struct { - Username string `dsn:"username"` - Timeout xtime.Duration `dsn:"query.timeout"` -} - -type cfg4 struct { - Timeout xtime.Duration `dsn:"query.timeout,1s"` -} - -func TestDecodeQuery(t *testing.T) { - type args struct { - query url.Values - v interface{} - assignFuncs map[string]assignFunc - } - tests := []struct { - name string - args args - want url.Values - cfg interface{} - wantErr bool - }{ - { - name: "test generic", - args: args{ - query: url.Values{ - "name": {"hello"}, - "Ignore": {"test"}, - "floatNum": {"22.33"}, - "adb": {"123"}, - }, - v: &cfg1{}, - }, - want: url.Values{ - "Ignore": {"test"}, - "adb": {"123"}, - }, - cfg: &cfg1{ - Name: "hello", - Def: "hello", - DefSlice: []int{1, 2, 3, 4}, - FloatNum: 22.33, - }, - }, - { - name: "test github.com/go-kratos/kratos/pkg/time", - args: args{ - query: url.Values{ - "timeout": {"1s"}, - }, - v: &cfg2{}, - }, - want: url.Values{}, - cfg: &cfg2{xtime.Duration(time.Second)}, - }, - { - name: "test empty github.com/go-kratos/kratos/pkg/time", - args: args{ - query: url.Values{}, - v: &cfg2{}, - }, - want: url.Values{}, - cfg: &cfg2{}, - }, - { - name: "test github.com/go-kratos/kratos/pkg/time", - args: args{ - query: url.Values{}, - v: &cfg4{}, - }, - want: url.Values{}, - cfg: &cfg4{xtime.Duration(time.Second)}, - }, - { - name: "test build-in value", - args: args{ - query: url.Values{ - "timeout": {"1s"}, - }, - v: &cfg3{}, - assignFuncs: map[string]assignFunc{"username": stringsAssignFunc("hello")}, - }, - want: url.Values{}, - cfg: &cfg3{ - Timeout: xtime.Duration(time.Second), - Username: "hello", - }, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - got, err := bindQuery(tt.args.query, tt.args.v, tt.args.assignFuncs) - if (err != nil) != tt.wantErr { - t.Errorf("DecodeQuery() error = %v, wantErr %v", err, tt.wantErr) - return - } - if !reflect.DeepEqual(got, tt.want) { - t.Errorf("DecodeQuery() = %v, want %v", got, tt.want) - } - if !reflect.DeepEqual(tt.args.v, tt.cfg) { - t.Errorf("DecodeQuery() = %v, want %v", tt.args.v, tt.cfg) - } - }) - } -} diff --git a/pkg/conf/env/README.md b/pkg/conf/env/README.md deleted file mode 100644 index 5afa4ba9b..000000000 --- a/pkg/conf/env/README.md +++ /dev/null @@ -1,5 +0,0 @@ -# env - -## 项目简介 - -全局公用环境变量 diff --git a/pkg/conf/env/env.go b/pkg/conf/env/env.go deleted file mode 100644 index 860895ccc..000000000 --- a/pkg/conf/env/env.go +++ /dev/null @@ -1,76 +0,0 @@ -// Package env get env & app config, all the public field must after init() -// finished and flag.Parse(). -package env - -import ( - "flag" - "os" - "strconv" - "time" -) - -// deploy env. -const ( - DeployEnvDev = "dev" - DeployEnvFat = "fat" - DeployEnvUat = "uat" - DeployEnvPre = "pre" - DeployEnvProd = "prod" -) - -// env default value. -const ( - // env - _region = "region01" - _zone = "zone01" - _deployEnv = "dev" -) - -// env configuration. -var ( - // Region available region where app at. - Region string - // Zone available zone where app at. - Zone string - // Hostname machine hostname. - Hostname string - // DeployEnv deploy env where app at. - DeployEnv string - // AppID is global unique application id, register by service tree. - // such as main.arch.disocvery. - AppID string - // Color is the identification of different experimental group in one caster cluster. - Color string - // DiscoveryNodes is seed nodes. - DiscoveryNodes string -) - -func init() { - var err error - Hostname = os.Getenv("HOSTNAME") - if Hostname == "" { - Hostname, err = os.Hostname() - if err != nil { - Hostname = strconv.Itoa(int(time.Now().UnixNano())) - } - } - addFlag(flag.CommandLine) -} - -func addFlag(fs *flag.FlagSet) { - // env - fs.StringVar(&Region, "region", defaultString("REGION", _region), "available region. or use REGION env variable, value: sh etc.") - fs.StringVar(&Zone, "zone", defaultString("ZONE", _zone), "available zone. or use ZONE env variable, value: sh001/sh002 etc.") - fs.StringVar(&AppID, "appid", os.Getenv("APP_ID"), "appid is global unique application id, register by service tree. or use APP_ID env variable.") - fs.StringVar(&DeployEnv, "deploy.env", defaultString("DEPLOY_ENV", _deployEnv), "deploy env. or use DEPLOY_ENV env variable, value: dev/fat1/uat/pre/prod etc.") - fs.StringVar(&Color, "deploy.color", os.Getenv("DEPLOY_COLOR"), "deploy.color is the identification of different experimental group.") - fs.StringVar(&DiscoveryNodes, "discovery.nodes", os.Getenv("DISCOVERY_NODES"), "discovery.nodes is seed nodes. value: 127.0.0.1:7171,127.0.0.2:7171 etc.") -} - -func defaultString(env, value string) string { - v := os.Getenv(env) - if v == "" { - return value - } - return v -} diff --git a/pkg/conf/env/env_test.go b/pkg/conf/env/env_test.go deleted file mode 100644 index a99de0caf..000000000 --- a/pkg/conf/env/env_test.go +++ /dev/null @@ -1,104 +0,0 @@ -package env - -import ( - "flag" - "fmt" - "os" - "testing" -) - -func TestDefaultString(t *testing.T) { - v := defaultString("a", "test") - if v != "test" { - t.Fatal("v must be test") - } - if err := os.Setenv("a", "test1"); err != nil { - t.Fatal(err) - } - v = defaultString("a", "test") - if v != "test1" { - t.Fatal("v must be test1") - } -} - -func TestEnv(t *testing.T) { - tests := []struct { - flag string - env string - def string - val *string - }{ - { - "region", - "REGION", - _region, - &Region, - }, - { - "zone", - "ZONE", - _zone, - &Zone, - }, - { - "deploy.env", - "DEPLOY_ENV", - _deployEnv, - &DeployEnv, - }, - { - "appid", - "APP_ID", - "", - &AppID, - }, - { - "deploy.color", - "DEPLOY_COLOR", - "", - &Color, - }, - } - for _, test := range tests { - // flag set value - t.Run(fmt.Sprintf("%s: flag set", test.env), func(t *testing.T) { - fs := flag.NewFlagSet("", flag.ContinueOnError) - addFlag(fs) - err := fs.Parse([]string{fmt.Sprintf("-%s=%s", test.flag, "test")}) - if err != nil { - t.Fatal(err) - } - if *test.val != "test" { - t.Fatal("val must be test") - } - }) - // flag not set, env set - t.Run(fmt.Sprintf("%s: flag not set, env set", test.env), func(t *testing.T) { - *test.val = "" - os.Setenv(test.env, "test2") - fs := flag.NewFlagSet("", flag.ContinueOnError) - addFlag(fs) - err := fs.Parse([]string{}) - if err != nil { - t.Fatal(err) - } - if *test.val != "test2" { - t.Fatal("val must be test") - } - }) - // flag not set, env not set - t.Run(fmt.Sprintf("%s: flag not set, env not set", test.env), func(t *testing.T) { - *test.val = "" - os.Setenv(test.env, "") - fs := flag.NewFlagSet("", flag.ContinueOnError) - addFlag(fs) - err := fs.Parse([]string{}) - if err != nil { - t.Fatal(err) - } - if *test.val != test.def { - t.Fatal("val must be test") - } - }) - } -} diff --git a/pkg/conf/flagvar/flagvar.go b/pkg/conf/flagvar/flagvar.go deleted file mode 100644 index 7fa8dca74..000000000 --- a/pkg/conf/flagvar/flagvar.go +++ /dev/null @@ -1,18 +0,0 @@ -package flagvar - -import ( - "strings" -) - -// StringVars []string implement flag.Value -type StringVars []string - -func (s StringVars) String() string { - return strings.Join(s, ",") -} - -// Set implement flag.Value -func (s *StringVars) Set(val string) error { - *s = append(*s, val) - return nil -} diff --git a/pkg/conf/paladin/README.md b/pkg/conf/paladin/README.md deleted file mode 100644 index a49a330ba..000000000 --- a/pkg/conf/paladin/README.md +++ /dev/null @@ -1,138 +0,0 @@ -#### paladin - -##### 项目简介 - -paladin 是一个config SDK客户端,包括了file、mock几个抽象功能,方便使用本地文件或者sven\apollo配置中心,并且集成了对象自动reload功能。 - -local files: -``` -demo -conf=/data/conf/app/msm-servie.toml -// or dir -demo -conf=/data/conf/app/ -``` - -*注:使用远程配置中心的用户在执行应用,如这里的`demo`时务必**不要**带上`-conf`参数,具体见下文远程配置中心的例子* - -local file example: -``` -type exampleConf struct { - Bool bool - Int int64 - Float float64 - String string -} - -func (e *exampleConf) Set(text string) error { - var ec exampleConf - if err := toml.Unmarshal([]byte(text), &ec); err != nil { - return err - } - *e = ec - return nil -} - -func ExampleClient() { - if err := paladin.Init(); err != nil { - panic(err) - } - var ( - ec exampleConf - eo exampleConf - m paladin.TOML - strs []string - ) - // config unmarshal - if err := paladin.Get("example.toml").UnmarshalTOML(&ec); err != nil { - panic(err) - } - // config setter - if err := paladin.Watch("example.toml", &ec); err != nil { - panic(err) - } - // paladin map - if err := paladin.Watch("example.toml", &m); err != nil { - panic(err) - } - s, err := m.Value("key").String() - b, err := m.Value("key").Bool() - i, err := m.Value("key").Int64() - f, err := m.Value("key").Float64() - // value slice - err = m.Value("strings").Slice(&strs) - // watch key - for event := range paladin.WatchEvent(context.TODO(), "key") { - fmt.Println(event) - } -} -``` - -remote config center example: -``` -type exampleConf struct { - Bool bool - Int int64 - Float float64 - String string -} - -func (e *exampleConf) Set(text string) error { - var ec exampleConf - if err := yaml.Unmarshal([]byte(text), &ec); err != nil { - return err - } - *e = ec - return nil -} - -func ExampleApolloClient() { - /* - pass flags or set envs that apollo needs, for example: - - ``` - export APOLLO_APP_ID=SampleApp - export APOLLO_CLUSTER=default - export APOLLO_CACHE_DIR=/tmp - export APOLLO_META_ADDR=localhost:8080 - export APOLLO_NAMESPACES=example.yml - ``` - */ - - if err := paladin.Init(apollo.PaladinDriverApollo); err != nil { - panic(err) - } - var ( - ec exampleConf - eo exampleConf - m paladin.Map - strs []string - ) - // config unmarshal - if err := paladin.Get("example.yml").UnmarshalYAML(&ec); err != nil { - panic(err) - } - // config setter - if err := paladin.Watch("example.yml", &ec); err != nil { - panic(err) - } - // paladin map - if err := paladin.Watch("example.yml", &m); err != nil { - panic(err) - } - s, err := m.Value("key").String() - b, err := m.Value("key").Bool() - i, err := m.Value("key").Int64() - f, err := m.Value("key").Float64() - // value slice - err = m.Value("strings").Slice(&strs) - // watch key - for event := range paladin.WatchEvent(context.TODO(), "key") { - fmt.Println(event) - } -} -``` - -##### 编译环境 - -- **请只用 Golang v1.12.x 以上版本编译执行** - -##### 依赖包 diff --git a/pkg/conf/paladin/apollo/apollo.go b/pkg/conf/paladin/apollo/apollo.go deleted file mode 100644 index 83b1f978e..000000000 --- a/pkg/conf/paladin/apollo/apollo.go +++ /dev/null @@ -1,275 +0,0 @@ -package apollo - -import ( - "context" - "errors" - "flag" - "log" - "os" - "strings" - "sync" - "time" - - "github.com/philchia/agollo/v4" - - "github.com/go-kratos/kratos/pkg/conf/paladin" -) - -var ( - _ paladin.Client = &apollo{} - defaultValue = "" -) - -type apolloWatcher struct { - keys []string // in apollo, they're called namespaces - C chan paladin.Event -} - -func newApolloWatcher(keys []string) *apolloWatcher { - return &apolloWatcher{keys: keys, C: make(chan paladin.Event, 5)} -} - -func (aw *apolloWatcher) HasKey(key string) bool { - if len(aw.keys) == 0 { - return true - } - for _, k := range aw.keys { - if k == key { - return true - } - } - return false -} - -func (aw *apolloWatcher) Handle(event paladin.Event) { - select { - case aw.C <- event: - default: - log.Printf("paladin: event channel full discard ns %s update event", event.Key) - } -} - -// apollo is apollo config client. -type apollo struct { - client agollo.Client - values *paladin.Map - wmu sync.RWMutex - watchers map[*apolloWatcher]struct{} -} - -// Config is apollo config client config. -type Config struct { - AppID string `json:"app_id"` - Cluster string `json:"cluster"` - CacheDir string `json:"cache_dir"` - MetaAddr string `json:"meta_addr"` - Namespaces []string `json:"namespaces"` - AccesskeySecret string `json:"accesskey_secret"` -} - -type apolloDriver struct{} - -var ( - confAppID, confCluster, confCacheDir, confMetaAddr, confNamespaces, accesskeySecret string -) - -func init() { - addApolloFlags() - paladin.Register(PaladinDriverApollo, &apolloDriver{}) -} - -func addApolloFlags() { - flag.StringVar(&confAppID, "apollo.appid", "", "apollo app id") - flag.StringVar(&confCluster, "apollo.cluster", "", "apollo cluster") - flag.StringVar(&confCacheDir, "apollo.cachedir", "/tmp", "apollo cache dir") - flag.StringVar(&confMetaAddr, "apollo.metaaddr", "", "apollo meta server addr, e.g. localhost:8080") - flag.StringVar(&confNamespaces, "apollo.namespaces", "", "subscribed apollo namespaces, comma separated, e.g. app.yml,mysql.yml") - flag.StringVar(&accesskeySecret, "apollo.accesskeysecret", "", "apollo accesskeysecret") -} - -func buildConfigForApollo() (c *Config, err error) { - if appidFromEnv := os.Getenv("APOLLO_APP_ID"); appidFromEnv != "" { - confAppID = appidFromEnv - } - if confAppID == "" { - err = errors.New("invalid apollo appid, pass it via APOLLO_APP_ID=xxx with env or --apollo.appid=xxx with flag") - return - } - if clusterFromEnv := os.Getenv("APOLLO_CLUSTER"); clusterFromEnv != "" { - confCluster = clusterFromEnv - } - if confCluster == "" { - err = errors.New("invalid apollo cluster, pass it via APOLLO_CLUSTER=xxx with env or --apollo.cluster=xxx with flag") - return - } - if cacheDirFromEnv := os.Getenv("APOLLO_CACHE_DIR"); cacheDirFromEnv != "" { - confCacheDir = cacheDirFromEnv - } - if metaAddrFromEnv := os.Getenv("APOLLO_META_ADDR"); metaAddrFromEnv != "" { - confMetaAddr = metaAddrFromEnv - } - if confMetaAddr == "" { - err = errors.New("invalid apollo meta addr, pass it via APOLLO_META_ADDR=xxx with env or --apollo.metaaddr=xxx with flag") - return - } - if namespacesFromEnv := os.Getenv("APOLLO_NAMESPACES"); namespacesFromEnv != "" { - confNamespaces = namespacesFromEnv - } - namespaceNames := strings.Split(confNamespaces, ",") - if len(namespaceNames) == 0 { - err = errors.New("invalid apollo namespaces, pass it via APOLLO_NAMESPACES=xxx with env or --apollo.namespaces=xxx with flag") - return - } - if accesskeySecretEnv := os.Getenv("APOLLO_ACCESS_KEY_SECRET"); accesskeySecretEnv != "" { - accesskeySecret = accesskeySecretEnv - } - c = &Config{ - AppID: confAppID, - Cluster: confCluster, - CacheDir: confCacheDir, - MetaAddr: confMetaAddr, - Namespaces: namespaceNames, - AccesskeySecret: accesskeySecret, - } - return -} - -// New new an apollo config client. -// it watches apollo namespaces changes and updates local cache. -// BTW, in our context, namespaces in apollo means keys in paladin. -func (ad *apolloDriver) New() (paladin.Client, error) { - c, err := buildConfigForApollo() - if err != nil { - return nil, err - } - return ad.new(c) -} - -func (ad *apolloDriver) new(conf *Config) (paladin.Client, error) { - if conf == nil { - err := errors.New("invalid apollo conf") - return nil, err - } - client := agollo.NewClient(&agollo.Conf{ - AppID: conf.AppID, - Cluster: conf.Cluster, - NameSpaceNames: conf.Namespaces, // these namespaces will be subscribed at init - CacheDir: conf.CacheDir, - MetaAddr: conf.MetaAddr, - AccesskeySecret: conf.AccesskeySecret, - }) - err := client.Start() - if err != nil { - return nil, err - } - a := &apollo{ - client: client, - values: new(paladin.Map), - watchers: make(map[*apolloWatcher]struct{}), - } - raws, err := a.loadValues(conf.Namespaces) - if err != nil { - return nil, err - } - a.values.Store(raws) - // watch namespaces by default. - a.WatchEvent(context.TODO(), conf.Namespaces...) - go a.watchproc(conf.Namespaces) - return a, nil -} - -// loadValues load values from apollo namespaces to values -func (a *apollo) loadValues(keys []string) (values map[string]*paladin.Value, err error) { - values = make(map[string]*paladin.Value, len(keys)) - for _, k := range keys { - if values[k], err = a.loadValue(k); err != nil { - return - } - } - return -} - -// loadValue load value from apollo namespace content to value -func (a *apollo) loadValue(key string) (*paladin.Value, error) { - content := a.client.GetContent(agollo.WithNamespace(key)) - return paladin.NewValue(content, content), nil -} - -// reloadValue reload value by key and send event -func (a *apollo) reloadValue(key string) (err error) { - // NOTE: in some case immediately read content from client after receive event - // will get old content due to cache, sleep 100ms make sure get correct content. - time.Sleep(100 * time.Millisecond) - var ( - value *paladin.Value - rawValue string - ) - value, err = a.loadValue(key) - if err != nil { - return - } - rawValue, err = value.Raw() - if err != nil { - return - } - raws := a.values.Load() - raws[key] = value - a.values.Store(raws) - a.wmu.RLock() - n := 0 - for w := range a.watchers { - if w.HasKey(key) { - n++ - // FIXME(Colstuwjx): check change event and send detail type like EventAdd\Update\Delete. - w.Handle(paladin.Event{Event: paladin.EventUpdate, Key: key, Value: rawValue}) - } - } - a.wmu.RUnlock() - log.Printf("paladin: reload config: %s events: %d\n", key, n) - return -} - -// apollo config daemon to watch remote apollo notifications -func (a *apollo) watchproc(keys []string) { - a.client.OnUpdate(func(event *agollo.ChangeEvent) { - if err := a.reloadValue(event.Namespace); err != nil { - log.Printf("paladin: load key: %s error: %s, skipped", event.Namespace, err) - } - }) -} -// Get return value by key. -func (a *apollo) Get(key string) *paladin.Value { - return a.values.Get(key) -} - -// GetAll return value map. -func (a *apollo) GetAll() *paladin.Map { - return a.values -} - -// WatchEvent watch with the specified keys. -func (a *apollo) WatchEvent(ctx context.Context, keys ...string) <-chan paladin.Event { - aw := newApolloWatcher(keys) - err := a.client.SubscribeToNamespaces(keys...) - if err != nil { - log.Printf("subscribe namespaces %v failed, %v", keys, err) - return aw.C - } - a.wmu.Lock() - a.watchers[aw] = struct{}{} - a.wmu.Unlock() - return aw.C -} - -// Close close watcher. -func (a *apollo) Close() (err error) { - if err = a.client.Stop(); err != nil { - return - } - a.wmu.RLock() - for w := range a.watchers { - close(w.C) - } - a.wmu.RUnlock() - return -} \ No newline at end of file diff --git a/pkg/conf/paladin/apollo/apollo_test.go b/pkg/conf/paladin/apollo/apollo_test.go deleted file mode 100644 index 387e77ce2..000000000 --- a/pkg/conf/paladin/apollo/apollo_test.go +++ /dev/null @@ -1,73 +0,0 @@ -package apollo - -import ( - "context" - "fmt" - "log" - "os" - "testing" - "time" - - "github.com/go-kratos/kratos/pkg/conf/paladin/apollo/internal/mockserver" -) - -func TestMain(m *testing.M) { - setup() - code := m.Run() - teardown() - os.Exit(code) -} - -func setup() { - go func() { - if err := mockserver.Run(); err != nil { - log.Fatal(err) - } - }() - // wait for mock server to run - time.Sleep(time.Millisecond * 500) -} - -func teardown() { - mockserver.Close() -} - -func TestApolloMock(t *testing.T) { - var ( - testAppYAML = "app.yml" - testAppYAMLContent1 = "test: test12234\ntest2: test333" - testAppYAMLContent2 = "test: 1111" - testClientJSON = "client.json" - testClientJSONContent = `{"name":"agollo"}` - ) - os.Setenv("APOLLO_APP_ID", "SampleApp") - os.Setenv("APOLLO_CLUSTER", "default") - os.Setenv("APOLLO_CACHE_DIR", "/tmp") - os.Setenv("APOLLO_META_ADDR", "localhost:8010") - os.Setenv("APOLLO_NAMESPACES", fmt.Sprintf("%s,%s", testAppYAML, testClientJSON)) - mockserver.Set(testAppYAML, "content", testAppYAMLContent1) - mockserver.Set(testClientJSON, "content", testClientJSONContent) - ad := &apolloDriver{} - apollo, err := ad.New() - if err != nil { - t.Fatalf("new apollo error, %v", err) - } - value := apollo.Get(testAppYAML) - if content, _ := value.String(); content != testAppYAMLContent1 { - t.Fatalf("got app.yml unexpected value %s", content) - } - value = apollo.Get(testClientJSON) - if content, _ := value.String(); content != testClientJSONContent { - t.Fatalf("got app.yml unexpected value %s", content) - } - mockserver.Set(testAppYAML, "content", testAppYAMLContent2) - updates := apollo.WatchEvent(context.TODO(), testAppYAML) - select { - case <-updates: - case <-time.After(time.Millisecond * 30000): - } - value = apollo.Get(testAppYAML) - if content, _ := value.String(); content != testAppYAMLContent2 { - t.Fatalf("got app.yml unexpected updated value %s", content) - } -} diff --git a/pkg/conf/paladin/apollo/const.go b/pkg/conf/paladin/apollo/const.go deleted file mode 100644 index 1aac397fe..000000000 --- a/pkg/conf/paladin/apollo/const.go +++ /dev/null @@ -1,6 +0,0 @@ -package apollo - -const ( - // PaladinDriverApollo ... - PaladinDriverApollo = "apollo" -) diff --git a/pkg/conf/paladin/apollo/internal/mockserver/mockserver.go b/pkg/conf/paladin/apollo/internal/mockserver/mockserver.go deleted file mode 100644 index 2aecb40a8..000000000 --- a/pkg/conf/paladin/apollo/internal/mockserver/mockserver.go +++ /dev/null @@ -1,149 +0,0 @@ -package mockserver - -import ( - "context" - "encoding/json" - "net/http" - "strings" - "sync" - "time" -) - -type notification struct { - NamespaceName string `json:"namespaceName,omitempty"` - NotificationID int `json:"notificationId,omitempty"` -} - -type result struct { - // AppID string `json:"appId"` - // Cluster string `json:"cluster"` - NamespaceName string `json:"namespaceName"` - Configurations map[string]string `json:"configurations"` - ReleaseKey string `json:"releaseKey"` -} - -type mockServer struct { - server http.Server - - lock sync.Mutex - notifications map[string]int - config map[string]map[string]string -} - -func (s *mockServer) NotificationHandler(rw http.ResponseWriter, req *http.Request) { - s.lock.Lock() - defer s.lock.Unlock() - req.ParseForm() - var notifications []notification - if err := json.Unmarshal([]byte(req.FormValue("notifications")), ¬ifications); err != nil { - rw.WriteHeader(http.StatusInternalServerError) - return - } - var changes []notification - for _, noti := range notifications { - if currentID := s.notifications[noti.NamespaceName]; currentID != noti.NotificationID { - changes = append(changes, notification{NamespaceName: noti.NamespaceName, NotificationID: currentID}) - } - } - - if len(changes) == 0 { - rw.WriteHeader(http.StatusNotModified) - return - } - bts, err := json.Marshal(&changes) - if err != nil { - rw.WriteHeader(http.StatusInternalServerError) - return - } - rw.Write(bts) -} - -func (s *mockServer) ConfigHandler(rw http.ResponseWriter, req *http.Request) { - req.ParseForm() - - strs := strings.Split(req.RequestURI, "/") - var namespace, releaseKey = strings.Split(strs[4], "?")[0], req.FormValue("releaseKey") - config := s.Get(namespace) - - ret := result{NamespaceName: namespace, Configurations: config, ReleaseKey: releaseKey} - bts, err := json.Marshal(&ret) - if err != nil { - rw.WriteHeader(http.StatusInternalServerError) - return - } - rw.Write(bts) -} - -var server *mockServer - -func (s *mockServer) Set(namespace, key, value string) { - server.lock.Lock() - defer server.lock.Unlock() - - notificationID := s.notifications[namespace] - notificationID++ - s.notifications[namespace] = notificationID - - if kv, ok := s.config[namespace]; ok { - kv[key] = value - return - } - kv := map[string]string{key: value} - s.config[namespace] = kv -} - -func (s *mockServer) Get(namespace string) map[string]string { - server.lock.Lock() - defer server.lock.Unlock() - - return s.config[namespace] -} - -func (s *mockServer) Delete(namespace, key string) { - server.lock.Lock() - defer server.lock.Unlock() - - if kv, ok := s.config[namespace]; ok { - delete(kv, key) - } - - notificationID := s.notifications[namespace] - notificationID++ - s.notifications[namespace] = notificationID -} - -// Set namespace's key value -func Set(namespace, key, value string) { - server.Set(namespace, key, value) -} - -// Delete namespace's key -func Delete(namespace, key string) { - server.Delete(namespace, key) -} - -// Run mock server -func Run() error { - initServer() - return server.server.ListenAndServe() -} - -func initServer() { - server = &mockServer{ - notifications: map[string]int{}, - config: map[string]map[string]string{}, - } - mux := http.NewServeMux() - mux.Handle("/notifications/", http.HandlerFunc(server.NotificationHandler)) - mux.Handle("/configs/", http.HandlerFunc(server.ConfigHandler)) - server.server.Handler = mux - server.server.Addr = ":8010" -} - -// Close mock server -func Close() error { - ctx, cancel := context.WithDeadline(context.Background(), time.Now().Add(time.Second)) - defer cancel() - - return server.server.Shutdown(ctx) -} diff --git a/pkg/conf/paladin/client.go b/pkg/conf/paladin/client.go deleted file mode 100644 index 62c0532fe..000000000 --- a/pkg/conf/paladin/client.go +++ /dev/null @@ -1,49 +0,0 @@ -package paladin - -import ( - "context" -) - -const ( - // EventAdd config add event. - EventAdd EventType = iota - // EventUpdate config update event. - EventUpdate - // EventRemove config remove event. - EventRemove -) - -// EventType is config event. -type EventType int - -// Event is watch event. -type Event struct { - Event EventType - Key string - Value string -} - -// Watcher is config watcher. -type Watcher interface { - WatchEvent(context.Context, ...string) <-chan Event - Close() error -} - -// Setter is value setter. -type Setter interface { - Set(string) error -} - -// Getter is value getter. -type Getter interface { - // Get a config value by a config key(may be a sven filename). - Get(string) *Value - // GetAll return all config key->value map. - GetAll() *Map -} - -// Client is config client. -type Client interface { - Watcher - Getter -} diff --git a/pkg/conf/paladin/default.go b/pkg/conf/paladin/default.go deleted file mode 100644 index a9410f563..000000000 --- a/pkg/conf/paladin/default.go +++ /dev/null @@ -1,92 +0,0 @@ -package paladin - -import ( - "context" - "errors" - "flag" -) - -var ( - // DefaultClient default client. - DefaultClient Client - confPath string -) - -func init() { - flag.StringVar(&confPath, "conf", "", "default config path") -} - -// Init init config client. -// If confPath is set, it inits file client by default -// Otherwise we could pass args to init remote client -// args[0]: driver name, string type -func Init(args ...interface{}) (err error) { - if confPath != "" { - DefaultClient, err = NewFile(confPath) - } else { - var ( - driver Driver - ) - argsLackErr := errors.New("lack of remote config center args") - if len(args) == 0 { - panic(argsLackErr.Error()) - } - argsInvalidErr := errors.New("invalid remote config center args") - driverName, ok := args[0].(string) - if !ok { - panic(argsInvalidErr.Error()) - } - driver, err = GetDriver(driverName) - if err != nil { - return - } - DefaultClient, err = driver.New() - } - if err != nil { - return - } - return -} - -// Watch watch on a key. The configuration implements the setter interface, which is invoked when the configuration changes. -func Watch(key string, s Setter) error { - v := DefaultClient.Get(key) - str, err := v.Raw() - if err != nil { - return err - } - if err := s.Set(str); err != nil { - return err - } - go func() { - for event := range WatchEvent(context.Background(), key) { - s.Set(event.Value) - } - }() - return nil -} - -// WatchEvent watch on multi keys. Events are returned when the configuration changes. -func WatchEvent(ctx context.Context, keys ...string) <-chan Event { - return DefaultClient.WatchEvent(ctx, keys...) -} - -// Get return value by key. -func Get(key string) *Value { - return DefaultClient.Get(key) -} - -// GetAll return all config map. -func GetAll() *Map { - return DefaultClient.GetAll() -} - -// Keys return values key. -func Keys() []string { - return DefaultClient.GetAll().Keys() -} - -// Close close watcher. -func Close() error { - return DefaultClient.Close() -} diff --git a/pkg/conf/paladin/driver.go b/pkg/conf/paladin/driver.go deleted file mode 100644 index 9f2151e4c..000000000 --- a/pkg/conf/paladin/driver.go +++ /dev/null @@ -1,9 +0,0 @@ -package paladin - -// Driver defined paladin remote client impl -// each remote config center driver must do -// 1. implements `New` method -// 2. call `Register` to register itself -type Driver interface { - New() (Client, error) -} diff --git a/pkg/conf/paladin/example_test.go b/pkg/conf/paladin/example_test.go deleted file mode 100644 index 5262785a7..000000000 --- a/pkg/conf/paladin/example_test.go +++ /dev/null @@ -1,147 +0,0 @@ -package paladin_test - -import ( - "context" - "fmt" - - "github.com/go-kratos/kratos/pkg/conf/paladin" - "github.com/go-kratos/kratos/pkg/conf/paladin/apollo" - - "github.com/BurntSushi/toml" -) - -type exampleConf struct { - Bool bool - Int int64 - Float float64 - String string - Strings []string -} - -func (e *exampleConf) Set(text string) error { - var ec exampleConf - if err := toml.Unmarshal([]byte(text), &ec); err != nil { - return err - } - *e = ec - return nil -} - -// ExampleClient is an example client usage. -// exmaple.toml: -/* - bool = true - int = 100 - float = 100.1 - string = "text" - strings = ["a", "b", "c"] -*/ -func ExampleClient() { - if err := paladin.Init(); err != nil { - panic(err) - } - var ec exampleConf - // var setter - if err := paladin.Watch("example.toml", &ec); err != nil { - panic(err) - } - if err := paladin.Get("example.toml").UnmarshalTOML(&ec); err != nil { - panic(err) - } - // use exampleConf - // watch event key - go func() { - for event := range paladin.WatchEvent(context.TODO(), "key") { - fmt.Println(event) - } - }() -} - -// ExampleApolloClient is an example client for apollo driver usage. -func ExampleApolloClient() { - /* - pass flags or set envs that apollo needs, for example: - - ``` - export APOLLO_APP_ID=SampleApp - export APOLLO_CLUSTER=default - export APOLLO_CACHE_DIR=/tmp - export APOLLO_META_ADDR=localhost:8080 - export APOLLO_NAMESPACES=example.yml - ``` - */ - - if err := paladin.Init(apollo.PaladinDriverApollo); err != nil { - panic(err) - } - var ec exampleConf - // var setter - if err := paladin.Watch("example.yml", &ec); err != nil { - panic(err) - } - if err := paladin.Get("example.yml").UnmarshalYAML(&ec); err != nil { - panic(err) - } - // use exampleConf - // watch event key - go func() { - for event := range paladin.WatchEvent(context.TODO(), "key") { - fmt.Println(event) - } - }() -} - -// ExampleMap is an example map usage. -// exmaple.toml: -/* - bool = true - int = 100 - float = 100.1 - string = "text" - strings = ["a", "b", "c"] - - [object] - string = "text" - bool = true - int = 100 - float = 100.1 - strings = ["a", "b", "c"] -*/ -func ExampleMap() { - var ( - m paladin.TOML - strs []string - ) - // paladin toml - if err := paladin.Watch("example.toml", &m); err != nil { - panic(err) - } - // value string - s, err := m.Get("string").String() - if err != nil { - s = "default" - } - fmt.Println(s) - // value bool - b, err := m.Get("bool").Bool() - if err != nil { - b = false - } - fmt.Println(b) - // value int - i, err := m.Get("int").Int64() - if err != nil { - i = 100 - } - fmt.Println(i) - // value float - f, err := m.Get("float").Float64() - if err != nil { - f = 100.1 - } - fmt.Println(f) - // value slice - if err = m.Get("strings").Slice(&strs); err == nil { - fmt.Println(strs) - } -} diff --git a/pkg/conf/paladin/file.go b/pkg/conf/paladin/file.go deleted file mode 100644 index c79abe3e5..000000000 --- a/pkg/conf/paladin/file.go +++ /dev/null @@ -1,203 +0,0 @@ -package paladin - -import ( - "context" - "fmt" - "io/ioutil" - "log" - "os" - "path" - "path/filepath" - "strings" - "sync" - "time" - - "github.com/fsnotify/fsnotify" -) - -const ( - defaultChSize = 10 -) - -var _ Client = &file{} - -// file is file config client. -type file struct { - values *Map - rawVal map[string]*Value - - watchChs map[string][]chan Event - mx sync.Mutex - wg sync.WaitGroup - - base string - done chan struct{} -} - -func isHiddenFile(name string) bool { - // TODO: support windows. - return strings.HasPrefix(filepath.Base(name), ".") -} - -func readAllPaths(base string) ([]string, error) { - fi, err := os.Stat(base) - if err != nil { - return nil, fmt.Errorf("check local config file fail! error: %s", err) - } - // dirs or file to paths - var paths []string - if fi.IsDir() { - files, err := ioutil.ReadDir(base) - if err != nil { - return nil, fmt.Errorf("read dir %s error: %s", base, err) - } - for _, file := range files { - if !file.IsDir() && !isHiddenFile(file.Name()) { - paths = append(paths, path.Join(base, file.Name())) - } - } - } else { - paths = append(paths, base) - } - return paths, nil -} - -func loadValuesFromPaths(paths []string) (map[string]*Value, error) { - // laod config file to values - var err error - values := make(map[string]*Value, len(paths)) - for _, fpath := range paths { - if values[path.Base(fpath)], err = loadValue(fpath); err != nil { - return nil, err - } - } - return values, nil -} - -func loadValue(fpath string) (*Value, error) { - data, err := ioutil.ReadFile(fpath) - if err != nil { - return nil, err - } - content := string(data) - return &Value{val: content, raw: content}, nil -} - -// NewFile new a config file client. -// conf = /data/conf/app/ -// conf = /data/conf/app/xxx.toml -func NewFile(base string) (Client, error) { - // platform slash - base = filepath.FromSlash(base) - - paths, err := readAllPaths(base) - if err != nil { - return nil, err - } - if len(paths) == 0 { - return nil, fmt.Errorf("empty config path") - } - - rawVal, err := loadValuesFromPaths(paths) - if err != nil { - return nil, err - } - - valMap := &Map{} - valMap.Store(rawVal) - fc := &file{ - values: valMap, - rawVal: rawVal, - watchChs: make(map[string][]chan Event), - - base: base, - done: make(chan struct{}, 1), - } - - fc.wg.Add(1) - go fc.daemon() - - return fc, nil -} - -// Get return value by key. -func (f *file) Get(key string) *Value { - return f.values.Get(key) -} - -// GetAll return value map. -func (f *file) GetAll() *Map { - return f.values -} - -// WatchEvent watch multi key. -func (f *file) WatchEvent(ctx context.Context, keys ...string) <-chan Event { - f.mx.Lock() - defer f.mx.Unlock() - ch := make(chan Event, defaultChSize) - for _, key := range keys { - f.watchChs[key] = append(f.watchChs[key], ch) - } - return ch -} - -// Close close watcher. -func (f *file) Close() error { - f.done <- struct{}{} - f.wg.Wait() - return nil -} - -// file config daemon to watch file modification -func (f *file) daemon() { - defer f.wg.Done() - fswatcher, err := fsnotify.NewWatcher() - if err != nil { - log.Printf("create file watcher fail! reload function will lose efficacy error: %s", err) - return - } - if err = fswatcher.Add(f.base); err != nil { - log.Printf("create fsnotify for base path %s fail %s, reload function will lose efficacy", f.base, err) - return - } - log.Printf("start watch filepath: %s", f.base) - for event := range fswatcher.Events { - switch event.Op { - // use vim edit config will trigger rename - case fsnotify.Write, fsnotify.Create: - f.reloadFile(event.Name) - case fsnotify.Chmod: - default: - log.Printf("unsupport event %s ingored", event) - } - } -} - -func (f *file) reloadFile(name string) { - if isHiddenFile(name) { - return - } - // NOTE: in some case immediately read file content after receive event - // will get old content, sleep 100ms make sure get correct content. - time.Sleep(200 * time.Millisecond) - key := filepath.Base(name) - val, err := loadValue(name) - if err != nil { - log.Printf("load file %s error: %s, skipped", name, err) - return - } - f.rawVal[key] = val - f.values.Store(f.rawVal) - - f.mx.Lock() - chs := f.watchChs[key] - f.mx.Unlock() - - for _, ch := range chs { - select { - case ch <- Event{Event: EventUpdate, Key: key, Value: val.raw}: - default: - log.Printf("event channel full discard file %s update event", name) - } - } -} diff --git a/pkg/conf/paladin/file_test.go b/pkg/conf/paladin/file_test.go deleted file mode 100644 index 36232ed61..000000000 --- a/pkg/conf/paladin/file_test.go +++ /dev/null @@ -1,157 +0,0 @@ -package paladin - -import ( - "context" - "io/ioutil" - "os" - "path/filepath" - "testing" - "time" - - "github.com/stretchr/testify/assert" -) - -func TestNewFile(t *testing.T) { - // test data - path := "/tmp/test_conf/" - assert.Nil(t, os.MkdirAll(path, 0700)) - assert.Nil(t, ioutil.WriteFile(filepath.Join(path, "test.toml"), []byte(` - text = "hello" - number = 100 - slice = [1, 2, 3] - sliceStr = ["1", "2", "3"] - `), 0644)) - // test client - cli, err := NewFile(filepath.Join(path, "test.toml")) - assert.Nil(t, err) - assert.NotNil(t, cli) - // test map - m := Map{} - text, err := cli.Get("test.toml").String() - assert.Nil(t, err) - assert.Nil(t, m.Set(text), "text") - s, err := m.Get("text").String() - assert.Nil(t, err) - assert.Equal(t, s, "hello", "text") - n, err := m.Get("number").Int64() - assert.Nil(t, err) - assert.Equal(t, n, int64(100), "number") -} - -func TestNewFilePath(t *testing.T) { - // test data - path := "/tmp/test_conf/" - assert.Nil(t, os.MkdirAll(path, 0700)) - assert.Nil(t, ioutil.WriteFile(filepath.Join(path, "test.toml"), []byte(` - text = "hello" - number = 100 - `), 0644)) - assert.Nil(t, ioutil.WriteFile(filepath.Join(path, "abc.toml"), []byte(` - text = "hello" - number = 100 - `), 0644)) - // test client - cli, err := NewFile(path) - assert.Nil(t, err) - assert.NotNil(t, cli) - // test map - m := Map{} - text, err := cli.Get("test.toml").String() - assert.Nil(t, err) - assert.Nil(t, m.Set(text), "text") - s, err := m.Get("text").String() - assert.Nil(t, err, s) - assert.Equal(t, s, "hello", "text") - n, err := m.Get("number").Int64() - assert.Nil(t, err, s) - assert.Equal(t, n, int64(100), "number") -} - -/* -func TestFileEvent(t *testing.T) { - // test data - path := "/tmp/test_conf_event/" - assert.Nil(t, os.MkdirAll(path, 0700)) - assert.Nil(t, ioutil.WriteFile(filepath.Join(path, "test.toml"), []byte(` - text = "hello" - number = 100 - `), 0644)) - assert.Nil(t, ioutil.WriteFile(filepath.Join(path, "abc.toml"), []byte(` - text = "hello" - number = 100 - `), 0644)) - // test client - cli, err := NewFile(path) - assert.Nil(t, err) - assert.NotNil(t, cli) - ch := cli.WatchEvent(context.Background(), "test.toml", "abc.toml") - time.Sleep(time.Second) - timeout := time.NewTimer(time.Second) - - // for file test.toml - ioutil.WriteFile(filepath.Join(path, "test.toml"), []byte(`hello`), 0644) - select { - case <-timeout.C: - t.Fatalf("run test timeout") - case ev := <-ch: - if ev.Key == "test.toml" { - assert.Equal(t, EventUpdate, ev.Event) - assert.Equal(t, "hello", ev.Value) - } - } - content1, _ := cli.Get("test.toml").String() - assert.Equal(t, "hello", content1) - - // for file abc.toml - ioutil.WriteFile(filepath.Join(path, "abc.toml"), []byte(`test`), 0644) - select { - case <-timeout.C: - t.Fatalf("run test timeout") - case ev := <-ch: - if ev.Key == "abc.toml" { - assert.Equal(t, EventUpdate, ev.Event) - assert.Equal(t, "test", ev.Value) - } - } - content2, _ := cli.Get("abc.toml").String() - assert.Equal(t, "test", content2) -} -*/ - -func TestHiddenFile(t *testing.T) { - path := "/tmp/test_hidden_event/" - assert.Nil(t, os.MkdirAll(path, 0700)) - assert.Nil(t, ioutil.WriteFile(filepath.Join(path, "test.toml"), []byte(`hello`), 0644)) - assert.Nil(t, ioutil.WriteFile(filepath.Join(path, "abc.toml"), []byte(` - text = "hello" - number = 100 - `), 0644)) - // test client - cli, err := NewFile(path) - assert.Nil(t, err) - assert.NotNil(t, cli) - cli.WatchEvent(context.Background(), "test.toml") - time.Sleep(time.Millisecond) - ioutil.WriteFile(filepath.Join(path, "abc.toml"), []byte(`hello`), 0644) - time.Sleep(time.Second) - content1, _ := cli.Get("test.toml").String() - assert.Equal(t, "hello", content1) - _, err = cli.Get(".abc.toml").String() - assert.NotNil(t, err) -} - -func TestOneLevelSymbolicFile(t *testing.T) { - path := "/tmp/test_symbolic_link/" - path2 := "/tmp/test_symbolic_link/configs/" - assert.Nil(t, os.MkdirAll(path2, 0700)) - assert.Nil(t, ioutil.WriteFile(filepath.Join(path, "test.toml"), []byte(`hello`), 0644)) - assert.Nil(t, os.Symlink(filepath.Join(path, "test.toml"), filepath.Join(path2, "test.toml.ln"))) - // test client - cli, err := NewFile(path2) - assert.Nil(t, err) - assert.NotNil(t, cli) - content, _ := cli.Get("test.toml.ln").String() - assert.Equal(t, "hello", content) - os.Remove(filepath.Join(path, "test.toml")) - os.Remove(filepath.Join(path2, "test.toml.ln")) -} diff --git a/pkg/conf/paladin/helper.go b/pkg/conf/paladin/helper.go deleted file mode 100644 index 115d438fc..000000000 --- a/pkg/conf/paladin/helper.go +++ /dev/null @@ -1,76 +0,0 @@ -package paladin - -import "time" - -// Bool return bool value. -func Bool(v *Value, def bool) bool { - b, err := v.Bool() - if err != nil { - return def - } - return b -} - -// Int return int value. -func Int(v *Value, def int) int { - i, err := v.Int() - if err != nil { - return def - } - return i -} - -// Int32 return int32 value. -func Int32(v *Value, def int32) int32 { - i, err := v.Int32() - if err != nil { - return def - } - return i -} - -// Int64 return int64 value. -func Int64(v *Value, def int64) int64 { - i, err := v.Int64() - if err != nil { - return def - } - return i -} - -// Float32 return float32 value. -func Float32(v *Value, def float32) float32 { - f, err := v.Float32() - if err != nil { - return def - } - return f -} - -// Float64 return float32 value. -func Float64(v *Value, def float64) float64 { - f, err := v.Float64() - if err != nil { - return def - } - return f -} - -// String return string value. -func String(v *Value, def string) string { - s, err := v.String() - if err != nil { - return def - } - return s -} - -// Duration parses a duration string. A duration string is a possibly signed sequence of decimal numbers -// each with optional fraction and a unit suffix, such as "300ms", "-1.5h" or "2h45m". Valid time units are "ns", "us" (or "µs"), "ms", "s", "m", "h". -func Duration(v *Value, def time.Duration) time.Duration { - dur, err := v.Duration() - if err != nil { - return def - } - return dur -} diff --git a/pkg/conf/paladin/helper_test.go b/pkg/conf/paladin/helper_test.go deleted file mode 100644 index aaa17d4d4..000000000 --- a/pkg/conf/paladin/helper_test.go +++ /dev/null @@ -1,286 +0,0 @@ -package paladin - -import ( - "testing" - "time" -) - -func TestBool(t *testing.T) { - type args struct { - v *Value - def bool - } - tests := []struct { - name string - args args - want bool - }{ - { - name: "ok", - args: args{v: &Value{val: true}}, - want: true, - }, - { - name: "fail", - args: args{v: &Value{}}, - want: false, - }, - { - name: "default", - args: args{v: &Value{}, def: true}, - want: true, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - if got := Bool(tt.args.v, tt.args.def); got != tt.want { - t.Errorf("Bool() = %v, want %v", got, tt.want) - } - }) - } -} - -func TestInt(t *testing.T) { - type args struct { - v *Value - def int - } - tests := []struct { - name string - args args - want int - }{ - { - name: "ok", - args: args{v: &Value{val: int64(2233)}}, - want: 2233, - }, - { - name: "fail", - args: args{v: &Value{}}, - want: 0, - }, - { - name: "default", - args: args{v: &Value{}, def: 2233}, - want: 2233, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - if got := Int(tt.args.v, tt.args.def); got != tt.want { - t.Errorf("Int() = %v, want %v", got, tt.want) - } - }) - } -} - -func TestInt32(t *testing.T) { - type args struct { - v *Value - def int32 - } - tests := []struct { - name string - args args - want int32 - }{ - { - name: "ok", - args: args{v: &Value{val: int64(2233)}}, - want: 2233, - }, - { - name: "fail", - args: args{v: &Value{}}, - want: 0, - }, - { - name: "default", - args: args{v: &Value{}, def: 2233}, - want: 2233, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - if got := Int32(tt.args.v, tt.args.def); got != tt.want { - t.Errorf("Int32() = %v, want %v", got, tt.want) - } - }) - } -} - -func TestInt64(t *testing.T) { - type args struct { - v *Value - def int64 - } - tests := []struct { - name string - args args - want int64 - }{ - { - name: "ok", - args: args{v: &Value{val: int64(2233)}}, - want: 2233, - }, - { - name: "fail", - args: args{v: &Value{}}, - want: 0, - }, - { - name: "default", - args: args{v: &Value{}, def: 2233}, - want: 2233, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - if got := Int64(tt.args.v, tt.args.def); got != tt.want { - t.Errorf("Int64() = %v, want %v", got, tt.want) - } - }) - } -} - -func TestFloat32(t *testing.T) { - type args struct { - v *Value - def float32 - } - tests := []struct { - name string - args args - want float32 - }{ - { - name: "ok", - args: args{v: &Value{val: float64(2233)}}, - want: float32(2233), - }, - { - name: "fail", - args: args{v: &Value{}}, - want: 0, - }, - { - name: "default", - args: args{v: &Value{}, def: float32(2233)}, - want: float32(2233), - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - if got := Float32(tt.args.v, tt.args.def); got != tt.want { - t.Errorf("Float32() = %v, want %v", got, tt.want) - } - }) - } -} - -func TestFloat64(t *testing.T) { - type args struct { - v *Value - def float64 - } - tests := []struct { - name string - args args - want float64 - }{ - { - name: "ok", - args: args{v: &Value{val: float64(2233)}}, - want: float64(2233), - }, - { - name: "fail", - args: args{v: &Value{}}, - want: 0, - }, - { - name: "default", - args: args{v: &Value{}, def: float64(2233)}, - want: float64(2233), - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - if got := Float64(tt.args.v, tt.args.def); got != tt.want { - t.Errorf("Float64() = %v, want %v", got, tt.want) - } - }) - } -} - -func TestString(t *testing.T) { - type args struct { - v *Value - def string - } - tests := []struct { - name string - args args - want string - }{ - { - name: "ok", - args: args{v: &Value{val: "test"}}, - want: "test", - }, - { - name: "fail", - args: args{v: &Value{}}, - want: "", - }, - { - name: "default", - args: args{v: &Value{}, def: "test"}, - want: "test", - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - if got := String(tt.args.v, tt.args.def); got != tt.want { - t.Errorf("String() = %v, want %v", got, tt.want) - } - }) - } -} - -func TestDuration(t *testing.T) { - type args struct { - v *Value - def time.Duration - } - tests := []struct { - name string - args args - want time.Duration - }{ - { - name: "ok", - args: args{v: &Value{val: "1s"}}, - want: time.Second, - }, - { - name: "fail", - args: args{v: &Value{}}, - want: 0, - }, - { - name: "default", - args: args{v: &Value{}, def: time.Second}, - want: time.Second, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - if got := Duration(tt.args.v, tt.args.def); got != tt.want { - t.Errorf("Duration() = %v, want %v", got, tt.want) - } - }) - } -} diff --git a/pkg/conf/paladin/map.go b/pkg/conf/paladin/map.go deleted file mode 100644 index fa43dc116..000000000 --- a/pkg/conf/paladin/map.go +++ /dev/null @@ -1,60 +0,0 @@ -package paladin - -import ( - "strings" - "sync/atomic" -) - -// KeyNamed key naming to lower case. -func KeyNamed(key string) string { - return strings.ToLower(key) -} - -// Map is config map, key(filename) -> value(file). -type Map struct { - values atomic.Value -} - -// Store sets the value of the Value to values map. -func (m *Map) Store(values map[string]*Value) { - dst := make(map[string]*Value, len(values)) - for k, v := range values { - dst[KeyNamed(k)] = v - } - m.values.Store(dst) -} - -// Load returns the value set by the most recent Store. -func (m *Map) Load() map[string]*Value { - src := m.values.Load().(map[string]*Value) - dst := make(map[string]*Value, len(src)) - for k, v := range src { - dst[k] = v - } - return dst -} - -// Exist check if values map exist a key. -func (m *Map) Exist(key string) bool { - _, ok := m.Load()[KeyNamed(key)] - return ok -} - -// Get return get value by key. -func (m *Map) Get(key string) *Value { - v, ok := m.Load()[KeyNamed(key)] - if ok { - return v - } - return &Value{} -} - -// Keys return map keys. -func (m *Map) Keys() []string { - values := m.Load() - keys := make([]string, 0, len(values)) - for key := range values { - keys = append(keys, key) - } - return keys -} diff --git a/pkg/conf/paladin/map_test.go b/pkg/conf/paladin/map_test.go deleted file mode 100644 index b4d7a3ac0..000000000 --- a/pkg/conf/paladin/map_test.go +++ /dev/null @@ -1,94 +0,0 @@ -package paladin_test - -import ( - "testing" - - "github.com/go-kratos/kratos/pkg/conf/paladin" - - "github.com/BurntSushi/toml" - "github.com/stretchr/testify/assert" -) - -type fruit struct { - Fruit []struct { - Name string - } -} - -func (f *fruit) Set(text string) error { - return toml.Unmarshal([]byte(text), f) -} - -func TestMap(t *testing.T) { - s := ` - # kv - text = "hello" - number = 100 - point = 100.1 - boolean = true - KeyCase = "test" - - # slice - numbers = [1, 2, 3] - strings = ["a", "b", "c"] - empty = [] - [[fruit]] - name = "apple" - [[fruit]] - name = "banana" - - # table - [database] - server = "192.168.1.1" - connection_max = 5000 - enabled = true - - [pool] - [pool.breaker] - xxx = "xxx" - ` - m := paladin.Map{} - assert.Nil(t, m.Set(s), s) - str, err := m.Get("text").String() - assert.Nil(t, err) - assert.Equal(t, str, "hello", "text") - n, err := m.Get("number").Int64() - assert.Nil(t, err) - assert.Equal(t, n, int64(100), "number") - p, err := m.Get("point").Float64() - assert.Nil(t, err) - assert.Equal(t, p, 100.1, "point") - b, err := m.Get("boolean").Bool() - assert.Nil(t, err) - assert.Equal(t, b, true, "boolean") - // key lower case - lb, err := m.Get("Boolean").Bool() - assert.Nil(t, err) - assert.Equal(t, lb, true, "boolean") - lt, err := m.Get("KeyCase").String() - assert.Nil(t, err) - assert.Equal(t, lt, "test", "key case") - var sliceInt []int64 - err = m.Get("numbers").Slice(&sliceInt) - assert.Nil(t, err) - assert.Equal(t, sliceInt, []int64{1, 2, 3}) - var sliceStr []string - err = m.Get("strings").Slice(&sliceStr) - assert.Nil(t, err) - assert.Equal(t, []string{"a", "b", "c"}, sliceStr) - err = m.Get("strings").Slice(&sliceStr) - assert.Nil(t, err) - assert.Equal(t, []string{"a", "b", "c"}, sliceStr) - // errors - err = m.Get("strings").Slice(sliceInt) - assert.NotNil(t, err) - err = m.Get("strings").Slice(&sliceInt) - assert.NotNil(t, err) - var obj struct { - Name string - } - err = m.Get("strings").Slice(obj) - assert.NotNil(t, err) - err = m.Get("strings").Slice(&obj) - assert.NotNil(t, err) -} diff --git a/pkg/conf/paladin/mock.go b/pkg/conf/paladin/mock.go deleted file mode 100644 index 4e705c1de..000000000 --- a/pkg/conf/paladin/mock.go +++ /dev/null @@ -1,40 +0,0 @@ -package paladin - -import ( - "context" -) - -var _ Client = &Mock{} - -// Mock is Mock config client. -type Mock struct { - C chan Event - *Map -} - -// NewMock new a config mock client. -func NewMock(vs map[string]string) Client { - values := make(map[string]*Value, len(vs)) - for k, v := range vs { - values[k] = &Value{val: v, raw: v} - } - m := new(Map) - m.Store(values) - return &Mock{Map: m, C: make(chan Event)} -} - -// GetAll return value map. -func (m *Mock) GetAll() *Map { - return m.Map -} - -// WatchEvent watch multi key. -func (m *Mock) WatchEvent(ctx context.Context, key ...string) <-chan Event { - return m.C -} - -// Close close watcher. -func (m *Mock) Close() error { - close(m.C) - return nil -} diff --git a/pkg/conf/paladin/mock_test.go b/pkg/conf/paladin/mock_test.go deleted file mode 100644 index 09785d716..000000000 --- a/pkg/conf/paladin/mock_test.go +++ /dev/null @@ -1,37 +0,0 @@ -package paladin_test - -import ( - "testing" - - "github.com/go-kratos/kratos/pkg/conf/paladin" - - "github.com/stretchr/testify/assert" -) - -func TestMock(t *testing.T) { - cs := map[string]string{ - "key_toml": ` - key_bool = true - key_int = 100 - key_float = 100.1 - key_string = "text" - `, - } - cli := paladin.NewMock(cs) - // test vlaue - var m paladin.TOML - err := cli.Get("key_toml").Unmarshal(&m) - assert.Nil(t, err) - b, err := m.Get("key_bool").Bool() - assert.Nil(t, err) - assert.Equal(t, b, true) - i, err := m.Get("key_int").Int64() - assert.Nil(t, err) - assert.Equal(t, i, int64(100)) - f, err := m.Get("key_float").Float64() - assert.Nil(t, err) - assert.Equal(t, f, float64(100.1)) - s, err := m.Get("key_string").String() - assert.Nil(t, err) - assert.Equal(t, s, "text") -} diff --git a/pkg/conf/paladin/register.go b/pkg/conf/paladin/register.go deleted file mode 100644 index 400497745..000000000 --- a/pkg/conf/paladin/register.go +++ /dev/null @@ -1,55 +0,0 @@ -package paladin - -import ( - "fmt" - "sort" - "sync" -) - -var ( - driversMu sync.RWMutex - drivers = make(map[string]Driver) -) - -// Register makes a paladin driver available by the provided name. -// If Register is called twice with the same name or if driver is nil, -// it panics. -func Register(name string, driver Driver) { - driversMu.Lock() - defer driversMu.Unlock() - - if driver == nil { - panic("paladin: driver is nil") - } - - if _, dup := drivers[name]; dup { - panic("paladin: Register called twice for driver " + name) - } - - drivers[name] = driver -} - -// Drivers returns a sorted list of the names of the registered paladin driver. -func Drivers() []string { - driversMu.RLock() - defer driversMu.RUnlock() - - var list []string - for name := range drivers { - list = append(list, name) - } - - sort.Strings(list) - return list -} - -// GetDriver returns a driver implement by name. -func GetDriver(name string) (Driver, error) { - driversMu.RLock() - driveri, ok := drivers[name] - driversMu.RUnlock() - if !ok { - return nil, fmt.Errorf("paladin: unknown driver %q (forgotten import?)", name) - } - return driveri, nil -} diff --git a/pkg/conf/paladin/toml.go b/pkg/conf/paladin/toml.go deleted file mode 100644 index 09595fb0b..000000000 --- a/pkg/conf/paladin/toml.go +++ /dev/null @@ -1,73 +0,0 @@ -package paladin - -import ( - "bytes" - "reflect" - "strconv" - - "github.com/BurntSushi/toml" - "github.com/pkg/errors" -) - -// TOML is toml map. -type TOML = Map - -// Set set the map by value. -func (m *TOML) Set(text string) error { - if err := m.UnmarshalText([]byte(text)); err != nil { - return err - } - return nil -} - -// UnmarshalText implemented toml. -func (m *TOML) UnmarshalText(text []byte) error { - raws := map[string]interface{}{} - if err := toml.Unmarshal(text, &raws); err != nil { - return err - } - values := map[string]*Value{} - for k, v := range raws { - k = KeyNamed(k) - rv := reflect.ValueOf(v) - switch rv.Kind() { - case reflect.Map: - buf := bytes.NewBuffer(nil) - err := toml.NewEncoder(buf).Encode(v) - // b, err := toml.Marshal(v) - if err != nil { - return err - } - // NOTE: value is map[string]interface{} - values[k] = &Value{val: v, raw: buf.String()} - case reflect.Slice: - raw := map[string]interface{}{ - k: v, - } - buf := bytes.NewBuffer(nil) - err := toml.NewEncoder(buf).Encode(raw) - // b, err := toml.Marshal(raw) - if err != nil { - return err - } - // NOTE: value is []interface{} - values[k] = &Value{val: v, raw: buf.String()} - case reflect.Bool: - b := v.(bool) - values[k] = &Value{val: b, raw: strconv.FormatBool(b)} - case reflect.Int64: - i := v.(int64) - values[k] = &Value{val: i, raw: strconv.FormatInt(i, 10)} - case reflect.Float64: - f := v.(float64) - values[k] = &Value{val: f, raw: strconv.FormatFloat(f, 'f', -1, 64)} - case reflect.String: - s := v.(string) - values[k] = &Value{val: s, raw: s} - default: - return errors.Errorf("UnmarshalTOML: unknown kind(%v)", rv.Kind()) - } - } - m.Store(values) - return nil -} diff --git a/pkg/conf/paladin/value.go b/pkg/conf/paladin/value.go deleted file mode 100644 index 733db7df7..000000000 --- a/pkg/conf/paladin/value.go +++ /dev/null @@ -1,185 +0,0 @@ -package paladin - -import ( - "encoding" - "encoding/json" - "reflect" - "time" - - "github.com/BurntSushi/toml" - "github.com/pkg/errors" - yaml "gopkg.in/yaml.v2" -) - -// ErrNotExist value key not exist. -var ( - ErrNotExist = errors.New("paladin: value key not exist") - ErrTypeAssertion = errors.New("paladin: value type assertion no match") - ErrDifferentTypes = errors.New("paladin: value different types") -) - -// Value is config value, maybe a json/toml/ini/string file. -type Value struct { - val interface{} - slice interface{} - raw string -} - -// NewValue new a value -func NewValue(val interface{}, raw string) *Value { - return &Value{ - val: val, - raw: raw, - } -} - -// Bool return bool value. -func (v *Value) Bool() (bool, error) { - if v.val == nil { - return false, ErrNotExist - } - b, ok := v.val.(bool) - if !ok { - return false, ErrTypeAssertion - } - return b, nil -} - -// Int return int value. -func (v *Value) Int() (int, error) { - i, err := v.Int64() - return int(i), err -} - -// Int32 return int32 value. -func (v *Value) Int32() (int32, error) { - i, err := v.Int64() - return int32(i), err -} - -// Int64 return int64 value. -func (v *Value) Int64() (int64, error) { - if v.val == nil { - return 0, ErrNotExist - } - i, ok := v.val.(int64) - if !ok { - return 0, ErrTypeAssertion - } - return i, nil -} - -// Float32 return float32 value. -func (v *Value) Float32() (float32, error) { - f, err := v.Float64() - if err != nil { - return 0.0, err - } - return float32(f), nil -} - -// Float64 return float64 value. -func (v *Value) Float64() (float64, error) { - if v.val == nil { - return 0.0, ErrNotExist - } - f, ok := v.val.(float64) - if !ok { - return 0.0, ErrTypeAssertion - } - return f, nil -} - -// String return string value. -func (v *Value) String() (string, error) { - if v.val == nil { - return "", ErrNotExist - } - s, ok := v.val.(string) - if !ok { - return "", ErrTypeAssertion - } - return s, nil -} - -// Duration parses a duration string. A duration string is a possibly signed sequence of decimal numbers -// each with optional fraction and a unit suffix, such as "300ms", "-1.5h" or "2h45m". Valid time units are "ns", "us" (or "µs"), "ms", "s", "m", "h". -func (v *Value) Duration() (time.Duration, error) { - s, err := v.String() - if err != nil { - return time.Duration(0), err - } - return time.ParseDuration(s) -} - -// Raw return raw value. -func (v *Value) Raw() (string, error) { - if v.val == nil { - return "", ErrNotExist - } - return v.raw, nil -} - -// Slice scan a slice interface, if slice has element it will be discard. -func (v *Value) Slice(dst interface{}) error { - // NOTE: val is []interface{}, slice is []type - if v.val == nil { - return ErrNotExist - } - rv := reflect.ValueOf(dst) - if rv.Kind() != reflect.Ptr || rv.Elem().Kind() != reflect.Slice { - return ErrDifferentTypes - } - el := rv.Elem() - // reset slice len to 0. - el.SetLen(0) - kind := el.Type().Elem().Kind() - src, ok := v.val.([]interface{}) - if !ok { - return ErrDifferentTypes - } - for _, s := range src { - if reflect.TypeOf(s).Kind() != kind { - return ErrTypeAssertion - } - el = reflect.Append(el, reflect.ValueOf(s)) - } - rv.Elem().Set(el) - return nil -} - -// Unmarshal is the interface implemented by an object that can unmarshal a textual representation of itself. -func (v *Value) Unmarshal(un encoding.TextUnmarshaler) error { - text, err := v.Raw() - if err != nil { - return err - } - return un.UnmarshalText([]byte(text)) -} - -// UnmarshalTOML unmarhsal toml to struct. -func (v *Value) UnmarshalTOML(dst interface{}) error { - text, err := v.Raw() - if err != nil { - return err - } - return toml.Unmarshal([]byte(text), dst) -} - -// UnmarshalJSON unmarhsal json to struct. -func (v *Value) UnmarshalJSON(dst interface{}) error { - text, err := v.Raw() - if err != nil { - return err - } - return json.Unmarshal([]byte(text), dst) -} - -// UnmarshalYAML unmarshal yaml to struct. -func (v *Value) UnmarshalYAML(dst interface{}) error { - text, err := v.Raw() - if err != nil { - return err - } - return yaml.Unmarshal([]byte(text), dst) -} diff --git a/pkg/conf/paladin/value_test.go b/pkg/conf/paladin/value_test.go deleted file mode 100644 index cdcdf4b27..000000000 --- a/pkg/conf/paladin/value_test.go +++ /dev/null @@ -1,206 +0,0 @@ -package paladin - -import ( - "fmt" - "testing" - "time" - - "github.com/stretchr/testify/assert" -) - -type testUnmarshler struct { - Text string - Int int -} - -func TestValueUnmarshal(t *testing.T) { - s := ` - int = 100 - text = "hello" - ` - v := Value{val: s, raw: s} - obj := new(testUnmarshler) - assert.Nil(t, v.UnmarshalTOML(obj)) - // error - v = Value{val: nil, raw: ""} - assert.NotNil(t, v.UnmarshalTOML(obj)) -} - -func TestValue(t *testing.T) { - var tests = []struct { - in interface{} - out interface{} - }{ - { - "text", - "text", - }, - { - time.Second * 10, - "10s", - }, - { - int64(100), - int64(100), - }, - { - float64(100.1), - float64(100.1), - }, - { - true, - true, - }, - { - nil, - nil, - }, - } - for _, test := range tests { - t.Run(fmt.Sprint(test.in), func(t *testing.T) { - v := Value{val: test.in, raw: fmt.Sprint(test.in)} - switch test.in.(type) { - case nil: - s, err := v.String() - assert.NotNil(t, err) - assert.Equal(t, s, "", test.in) - i, err := v.Int64() - assert.NotNil(t, err) - assert.Equal(t, i, int64(0), test.in) - f, err := v.Float64() - assert.NotNil(t, err) - assert.Equal(t, f, float64(0.0), test.in) - b, err := v.Bool() - assert.NotNil(t, err) - assert.Equal(t, b, false, test.in) - case string: - val, err := v.String() - assert.Nil(t, err) - assert.Equal(t, val, test.out.(string), test.in) - case int64: - val, err := v.Int() - assert.Nil(t, err) - assert.Equal(t, val, int(test.out.(int64)), test.in) - val32, err := v.Int32() - assert.Nil(t, err) - assert.Equal(t, val32, int32(test.out.(int64)), test.in) - val64, err := v.Int64() - assert.Nil(t, err) - assert.Equal(t, val64, test.out.(int64), test.in) - case float64: - val32, err := v.Float32() - assert.Nil(t, err) - assert.Equal(t, val32, float32(test.out.(float64)), test.in) - val64, err := v.Float64() - assert.Nil(t, err) - assert.Equal(t, val64, test.out.(float64), test.in) - case bool: - val, err := v.Bool() - assert.Nil(t, err) - assert.Equal(t, val, test.out.(bool), test.in) - case time.Duration: - v.val = test.out - val, err := v.Duration() - assert.Nil(t, err) - assert.Equal(t, val, test.in.(time.Duration), test.out) - } - }) - } -} - -func TestValueSlice(t *testing.T) { - var tests = []struct { - in interface{} - out interface{} - }{ - { - nil, - nil, - }, - { - []interface{}{"a", "b", "c"}, - []string{"a", "b", "c"}, - }, - { - []interface{}{1, 2, 3}, - []int64{1, 2, 3}, - }, - { - []interface{}{1.1, 1.2, 1.3}, - []float64{1.1, 1.2, 1.3}, - }, - { - []interface{}{true, false, true}, - []bool{true, false, true}, - }, - } - for _, test := range tests { - t.Run(fmt.Sprint(test.in), func(t *testing.T) { - v := Value{val: test.in, raw: fmt.Sprint(test.in)} - switch test.in.(type) { - case nil: - var s []string - assert.NotNil(t, v.Slice(&s)) - case []string: - var s []string - assert.Nil(t, v.Slice(&s)) - assert.Equal(t, s, test.out) - case []int64: - var s []int64 - assert.Nil(t, v.Slice(&s)) - assert.Equal(t, s, test.out) - case []float64: - var s []float64 - assert.Nil(t, v.Slice(&s)) - assert.Equal(t, s, test.out) - case []bool: - var s []bool - assert.Nil(t, v.Slice(&s)) - assert.Equal(t, s, test.out) - } - }) - } -} - -func BenchmarkValueInt(b *testing.B) { - v := &Value{val: int64(100), raw: "100"} - b.RunParallel(func(pb *testing.PB) { - for pb.Next() { - v.Int64() - } - }) -} -func BenchmarkValueFloat(b *testing.B) { - v := &Value{val: float64(100.1), raw: "100.1"} - b.RunParallel(func(pb *testing.PB) { - for pb.Next() { - v.Float64() - } - }) -} -func BenchmarkValueBool(b *testing.B) { - v := &Value{val: true, raw: "true"} - b.RunParallel(func(pb *testing.PB) { - for pb.Next() { - v.Bool() - } - }) -} -func BenchmarkValueString(b *testing.B) { - v := &Value{val: "text", raw: "text"} - b.RunParallel(func(pb *testing.PB) { - for pb.Next() { - v.String() - } - }) -} - -func BenchmarkValueSlice(b *testing.B) { - v := &Value{val: []interface{}{1, 2, 3}, raw: "100"} - b.RunParallel(func(pb *testing.PB) { - var slice []int64 - for pb.Next() { - v.Slice(&slice) - } - }) -} diff --git a/pkg/container/group/README.md b/pkg/container/group/README.md deleted file mode 100644 index 33d48ba01..000000000 --- a/pkg/container/group/README.md +++ /dev/null @@ -1,12 +0,0 @@ -#### group - -##### 项目简介 - -懒加载对象容器 - -##### 编译环境 - -- **推荐 Golang v1.12.1 以上版本编译执行** - -##### 依赖包 - diff --git a/pkg/container/group/example_test.go b/pkg/container/group/example_test.go deleted file mode 100644 index 9f52fb809..000000000 --- a/pkg/container/group/example_test.go +++ /dev/null @@ -1,46 +0,0 @@ -package group - -import "fmt" - -type Counter struct { - Value int -} - -func (c *Counter) Incr() { - c.Value++ -} - -func ExampleGroup_Get() { - new := func() interface{} { - fmt.Println("Only Once") - return &Counter{} - } - group := NewGroup(new) - - // Create a new Counter - group.Get("pass").(*Counter).Incr() - - // Get the created Counter again. - group.Get("pass").(*Counter).Incr() - // Output: - // Only Once -} - -func ExampleGroup_Reset() { - new := func() interface{} { - return &Counter{} - } - group := NewGroup(new) - - newV2 := func() interface{} { - fmt.Println("New V2") - return &Counter{} - } - // Reset the new function and clear all created objects. - group.Reset(newV2) - - // Create a new Counter - group.Get("pass").(*Counter).Incr() - // Output: - // New V2 -} diff --git a/pkg/container/group/group.go b/pkg/container/group/group.go deleted file mode 100644 index ad0324df0..000000000 --- a/pkg/container/group/group.go +++ /dev/null @@ -1,64 +0,0 @@ -// Package group provides a sample lazy load container. -// The group only creating a new object not until the object is needed by user. -// And it will cache all the objects to reduce the creation of object. -package group - -import "sync" - -// Group is a lazy load container. -type Group struct { - new func() interface{} - objs map[string]interface{} - sync.RWMutex -} - -// NewGroup news a group container. -func NewGroup(new func() interface{}) *Group { - if new == nil { - panic("container.group: can't assign a nil to the new function") - } - return &Group{ - new: new, - objs: make(map[string]interface{}), - } -} - -// Get gets the object by the given key. -func (g *Group) Get(key string) interface{} { - g.RLock() - obj, ok := g.objs[key] - if ok { - g.RUnlock() - return obj - } - g.RUnlock() - - // double check - g.Lock() - defer g.Unlock() - obj, ok = g.objs[key] - if ok { - return obj - } - obj = g.new() - g.objs[key] = obj - return obj -} - -// Reset resets the new function and deletes all existing objects. -func (g *Group) Reset(new func() interface{}) { - if new == nil { - panic("container.group: can't assign a nil to the new function") - } - g.Lock() - g.new = new - g.Unlock() - g.Clear() -} - -// Clear deletes all objects. -func (g *Group) Clear() { - g.Lock() - g.objs = make(map[string]interface{}) - g.Unlock() -} diff --git a/pkg/container/group/group_test.go b/pkg/container/group/group_test.go deleted file mode 100644 index 50464dbe1..000000000 --- a/pkg/container/group/group_test.go +++ /dev/null @@ -1,65 +0,0 @@ -package group - -import ( - "testing" - - "github.com/stretchr/testify/assert" -) - -func TestGroupGet(t *testing.T) { - count := 0 - g := NewGroup(func() interface{} { - count++ - return count - }) - v := g.Get("/x/internal/dummy/user") - assert.Equal(t, 1, v.(int)) - - v = g.Get("/x/internal/dummy/avatar") - assert.Equal(t, 2, v.(int)) - - v = g.Get("/x/internal/dummy/user") - assert.Equal(t, 1, v.(int)) - assert.Equal(t, 2, count) -} - -func TestGroupReset(t *testing.T) { - g := NewGroup(func() interface{} { - return 1 - }) - g.Get("/x/internal/dummy/user") - call := false - g.Reset(func() interface{} { - call = true - return 1 - }) - - length := 0 - for range g.objs { - length++ - } - - assert.Equal(t, 0, length) - - g.Get("/x/internal/dummy/user") - assert.Equal(t, true, call) -} - -func TestGroupClear(t *testing.T) { - g := NewGroup(func() interface{} { - return 1 - }) - g.Get("/x/internal/dummy/user") - length := 0 - for range g.objs { - length++ - } - assert.Equal(t, 1, length) - - g.Clear() - length = 0 - for range g.objs { - length++ - } - assert.Equal(t, 0, length) -} diff --git a/pkg/container/pool/README.md b/pkg/container/pool/README.md deleted file mode 100644 index 05d5198f9..000000000 --- a/pkg/container/pool/README.md +++ /dev/null @@ -1,5 +0,0 @@ -# pool - -## 项目简介 - -通用连接池实现 diff --git a/pkg/container/pool/list.go b/pkg/container/pool/list.go deleted file mode 100644 index 7155953e6..000000000 --- a/pkg/container/pool/list.go +++ /dev/null @@ -1,226 +0,0 @@ -package pool - -import ( - "container/list" - "context" - "io" - "sync" - "time" -) - -var _ Pool = &List{} - -// List . -type List struct { - // New is an application supplied function for creating and configuring a - // item. - // - // The item returned from new must not be in a special state - // (subscribed to pubsub channel, transaction started, ...). - New func(ctx context.Context) (io.Closer, error) - - // mu protects fields defined below. - mu sync.Mutex - cond chan struct{} - closed bool - active int - // clean stale items - cleanerCh chan struct{} - - // Stack of item with most recently used at the front. - idles list.List - - // Config pool configuration - conf *Config -} - -// NewList creates a new pool. -func NewList(c *Config) *List { - // check Config - if c == nil || c.Active < c.Idle { - panic("config nil or Idle Must <= Active") - } - // new pool - p := &List{conf: c} - p.cond = make(chan struct{}) - p.startCleanerLocked(time.Duration(c.IdleTimeout)) - return p -} - -// Reload reload config. -func (p *List) Reload(c *Config) error { - p.mu.Lock() - p.conf = c - p.mu.Unlock() - return nil -} - -// startCleanerLocked -func (p *List) startCleanerLocked(d time.Duration) { - if d <= 0 { - // if set 0, staleCleaner() will return directly - return - } - if d < time.Duration(p.conf.IdleTimeout) && p.cleanerCh != nil { - select { - case p.cleanerCh <- struct{}{}: - default: - } - } - // run only one, clean stale items. - if p.cleanerCh == nil { - p.cleanerCh = make(chan struct{}, 1) - go p.staleCleaner() - } -} - -// staleCleaner clean stale items proc. -func (p *List) staleCleaner() { - ticker := time.NewTicker(100 * time.Millisecond) - for { - select { - case <-ticker.C: - case <-p.cleanerCh: // maxLifetime was changed or db was closed. - } - p.mu.Lock() - if p.closed || p.conf.IdleTimeout <= 0 { - p.mu.Unlock() - return - } - for i, n := 0, p.idles.Len(); i < n; i++ { - e := p.idles.Back() - if e == nil { - // no possible - break - } - ic := e.Value.(item) - if !ic.expired(time.Duration(p.conf.IdleTimeout)) { - // not need continue. - break - } - p.idles.Remove(e) - p.release() - p.mu.Unlock() - ic.c.Close() - p.mu.Lock() - } - p.mu.Unlock() - } -} - -// Get returns a item from the idles List or -// get a new item. -func (p *List) Get(ctx context.Context) (io.Closer, error) { - p.mu.Lock() - if p.closed { - p.mu.Unlock() - return nil, ErrPoolClosed - } - for { - // get idles item. - for i, n := 0, p.idles.Len(); i < n; i++ { - e := p.idles.Front() - if e == nil { - break - } - ic := e.Value.(item) - p.idles.Remove(e) - p.mu.Unlock() - if !ic.expired(time.Duration(p.conf.IdleTimeout)) { - return ic.c, nil - } - ic.c.Close() - p.mu.Lock() - p.release() - } - // Check for pool closed before dialing a new item. - if p.closed { - p.mu.Unlock() - return nil, ErrPoolClosed - } - // new item if under limit. - if p.conf.Active == 0 || p.active < p.conf.Active { - newItem := p.New - p.active++ - p.mu.Unlock() - c, err := newItem(ctx) - if err != nil { - p.mu.Lock() - p.release() - p.mu.Unlock() - c = nil - } - return c, err - } - if p.conf.WaitTimeout == 0 && !p.conf.Wait { - p.mu.Unlock() - return nil, ErrPoolExhausted - } - wt := p.conf.WaitTimeout - p.mu.Unlock() - - // slowpath: reset context timeout - nctx := ctx - cancel := func() {} - if wt > 0 { - _, nctx, cancel = wt.Shrink(ctx) - } - select { - case <-nctx.Done(): - cancel() - return nil, nctx.Err() - case <-p.cond: - } - cancel() - p.mu.Lock() - } -} - -// Put put item into pool. -func (p *List) Put(ctx context.Context, c io.Closer, forceClose bool) error { - p.mu.Lock() - if !p.closed && !forceClose { - p.idles.PushFront(item{createdAt: nowFunc(), c: c}) - if p.idles.Len() > p.conf.Idle { - c = p.idles.Remove(p.idles.Back()).(item).c - } else { - c = nil - } - } - if c == nil { - p.signal() - p.mu.Unlock() - return nil - } - p.release() - p.mu.Unlock() - return c.Close() -} - -// Close releases the resources used by the pool. -func (p *List) Close() error { - p.mu.Lock() - idles := p.idles - p.idles.Init() - p.closed = true - p.active -= idles.Len() - p.mu.Unlock() - for e := idles.Front(); e != nil; e = e.Next() { - e.Value.(item).c.Close() - } - return nil -} - -// release decrements the active count and signals waiters. The caller must -// hold p.mu during the call. -func (p *List) release() { - p.active-- - p.signal() -} - -func (p *List) signal() { - select { - default: - case p.cond <- struct{}{}: - } -} diff --git a/pkg/container/pool/list_test.go b/pkg/container/pool/list_test.go deleted file mode 100644 index 286e547b8..000000000 --- a/pkg/container/pool/list_test.go +++ /dev/null @@ -1,322 +0,0 @@ -package pool - -import ( - "context" - "io" - "testing" - "time" - - xtime "github.com/go-kratos/kratos/pkg/time" - - "github.com/stretchr/testify/assert" -) - -func TestListGetPut(t *testing.T) { - // new pool - config := &Config{ - Active: 1, - Idle: 1, - IdleTimeout: xtime.Duration(90 * time.Second), - WaitTimeout: xtime.Duration(10 * time.Millisecond), - Wait: false, - } - pool := NewList(config) - pool.New = func(ctx context.Context) (io.Closer, error) { - return &closer{}, nil - } - - // test Get Put - conn, err := pool.Get(context.TODO()) - assert.Nil(t, err) - c1 := connection{pool: pool, c: conn} - c1.HandleNormal() - c1.Close() -} - -func TestListPut(t *testing.T) { - var id = 0 - type connID struct { - io.Closer - id int - } - config := &Config{ - Active: 1, - Idle: 1, - IdleTimeout: xtime.Duration(1 * time.Second), - // WaitTimeout: xtime.Duration(10 * time.Millisecond), - Wait: false, - } - pool := NewList(config) - pool.New = func(ctx context.Context) (io.Closer, error) { - id = id + 1 - return &connID{id: id, Closer: &closer{}}, nil - } - // test Put(ctx, conn, true) - conn, err := pool.Get(context.TODO()) - assert.Nil(t, err) - conn1 := conn.(*connID) - // Put(ctx, conn, true) drop the connection. - pool.Put(context.TODO(), conn, true) - conn, err = pool.Get(context.TODO()) - assert.Nil(t, err) - conn2 := conn.(*connID) - assert.NotEqual(t, conn1.id, conn2.id) -} - -func TestListIdleTimeout(t *testing.T) { - var id = 0 - type connID struct { - io.Closer - id int - } - config := &Config{ - Active: 1, - Idle: 1, - // conn timeout - IdleTimeout: xtime.Duration(1 * time.Millisecond), - } - pool := NewList(config) - pool.New = func(ctx context.Context) (io.Closer, error) { - id = id + 1 - return &connID{id: id, Closer: &closer{}}, nil - } - // test Put(ctx, conn, true) - conn, err := pool.Get(context.TODO()) - assert.Nil(t, err) - conn1 := conn.(*connID) - // Put(ctx, conn, true) drop the connection. - pool.Put(context.TODO(), conn, false) - time.Sleep(5 * time.Millisecond) - // idletimeout and get new conn - conn, err = pool.Get(context.TODO()) - assert.Nil(t, err) - conn2 := conn.(*connID) - assert.NotEqual(t, conn1.id, conn2.id) -} - -func TestListContextTimeout(t *testing.T) { - // new pool - config := &Config{ - Active: 1, - Idle: 1, - IdleTimeout: xtime.Duration(90 * time.Second), - WaitTimeout: xtime.Duration(10 * time.Millisecond), - Wait: false, - } - pool := NewList(config) - pool.New = func(ctx context.Context) (io.Closer, error) { - return &closer{}, nil - } - // test context timeout - ctx, cancel := context.WithTimeout(context.TODO(), 100*time.Millisecond) - defer cancel() - conn, err := pool.Get(ctx) - assert.Nil(t, err) - _, err = pool.Get(ctx) - // context timeout error - assert.NotNil(t, err) - pool.Put(context.TODO(), conn, false) - _, err = pool.Get(ctx) - assert.Nil(t, err) -} - -func TestListPoolExhausted(t *testing.T) { - // test pool exhausted - config := &Config{ - Active: 1, - Idle: 1, - IdleTimeout: xtime.Duration(90 * time.Second), - // WaitTimeout: xtime.Duration(10 * time.Millisecond), - Wait: false, - } - pool := NewList(config) - pool.New = func(ctx context.Context) (io.Closer, error) { - return &closer{}, nil - } - - ctx, cancel := context.WithTimeout(context.TODO(), 100*time.Millisecond) - defer cancel() - conn, err := pool.Get(context.TODO()) - assert.Nil(t, err) - _, err = pool.Get(ctx) - // config active == 1, so no available conns make connection exhausted. - assert.NotNil(t, err) - pool.Put(context.TODO(), conn, false) - _, err = pool.Get(ctx) - assert.Nil(t, err) -} - -func TestListStaleClean(t *testing.T) { - var id = 0 - type connID struct { - io.Closer - id int - } - config := &Config{ - Active: 1, - Idle: 1, - IdleTimeout: xtime.Duration(1 * time.Second), - // WaitTimeout: xtime.Duration(10 * time.Millisecond), - Wait: false, - } - pool := NewList(config) - pool.New = func(ctx context.Context) (io.Closer, error) { - id = id + 1 - return &connID{id: id, Closer: &closer{}}, nil - } - conn, err := pool.Get(context.TODO()) - assert.Nil(t, err) - conn1 := conn.(*connID) - pool.Put(context.TODO(), conn, false) - conn, err = pool.Get(context.TODO()) - assert.Nil(t, err) - conn2 := conn.(*connID) - assert.Equal(t, conn1.id, conn2.id) - pool.Put(context.TODO(), conn, false) - // sleep more than idleTimeout - time.Sleep(2 * time.Second) - conn, err = pool.Get(context.TODO()) - assert.Nil(t, err) - conn3 := conn.(*connID) - assert.NotEqual(t, conn1.id, conn3.id) -} - -func BenchmarkList1(b *testing.B) { - config := &Config{ - Active: 30, - Idle: 30, - IdleTimeout: xtime.Duration(90 * time.Second), - WaitTimeout: xtime.Duration(10 * time.Millisecond), - Wait: false, - } - pool := NewList(config) - pool.New = func(ctx context.Context) (io.Closer, error) { - return &closer{}, nil - } - - b.ResetTimer() - b.RunParallel(func(pb *testing.PB) { - for pb.Next() { - conn, err := pool.Get(context.TODO()) - if err != nil { - b.Error(err) - continue - } - c1 := connection{pool: pool, c: conn} - c1.HandleQuick() - c1.Close() - } - }) -} - -func BenchmarkList2(b *testing.B) { - config := &Config{ - Active: 30, - Idle: 30, - IdleTimeout: xtime.Duration(90 * time.Second), - WaitTimeout: xtime.Duration(10 * time.Millisecond), - Wait: false, - } - pool := NewList(config) - pool.New = func(ctx context.Context) (io.Closer, error) { - return &closer{}, nil - } - - b.ResetTimer() - b.RunParallel(func(pb *testing.PB) { - for pb.Next() { - conn, err := pool.Get(context.TODO()) - if err != nil { - b.Error(err) - continue - } - c1 := connection{pool: pool, c: conn} - c1.HandleNormal() - c1.Close() - } - }) -} - -func BenchmarkPool3(b *testing.B) { - config := &Config{ - Active: 30, - Idle: 30, - IdleTimeout: xtime.Duration(90 * time.Second), - WaitTimeout: xtime.Duration(10 * time.Millisecond), - Wait: false, - } - pool := NewList(config) - pool.New = func(ctx context.Context) (io.Closer, error) { - return &closer{}, nil - } - - b.ResetTimer() - b.RunParallel(func(pb *testing.PB) { - for pb.Next() { - conn, err := pool.Get(context.TODO()) - if err != nil { - b.Error(err) - continue - } - c1 := connection{pool: pool, c: conn} - c1.HandleSlow() - c1.Close() - } - }) -} - -func BenchmarkList4(b *testing.B) { - config := &Config{ - Active: 30, - Idle: 30, - IdleTimeout: xtime.Duration(90 * time.Second), - // WaitTimeout: xtime.Duration(10 * time.Millisecond), - Wait: false, - } - pool := NewList(config) - pool.New = func(ctx context.Context) (io.Closer, error) { - return &closer{}, nil - } - - b.ResetTimer() - b.RunParallel(func(pb *testing.PB) { - for pb.Next() { - conn, err := pool.Get(context.TODO()) - if err != nil { - b.Error(err) - continue - } - c1 := connection{pool: pool, c: conn} - c1.HandleSlow() - c1.Close() - } - }) -} - -func BenchmarkList5(b *testing.B) { - config := &Config{ - Active: 30, - Idle: 30, - IdleTimeout: xtime.Duration(90 * time.Second), - // WaitTimeout: xtime.Duration(10 * time.Millisecond), - Wait: true, - } - pool := NewList(config) - pool.New = func(ctx context.Context) (io.Closer, error) { - return &closer{}, nil - } - - b.ResetTimer() - b.RunParallel(func(pb *testing.PB) { - for pb.Next() { - conn, err := pool.Get(context.TODO()) - if err != nil { - b.Error(err) - continue - } - c1 := connection{pool: pool, c: conn} - c1.HandleSlow() - c1.Close() - } - }) -} diff --git a/pkg/container/pool/pool.go b/pkg/container/pool/pool.go deleted file mode 100644 index 9aec0697c..000000000 --- a/pkg/container/pool/pool.go +++ /dev/null @@ -1,62 +0,0 @@ -package pool - -import ( - "context" - "errors" - "io" - "time" - - xtime "github.com/go-kratos/kratos/pkg/time" -) - -var ( - // ErrPoolExhausted connections are exhausted. - ErrPoolExhausted = errors.New("container/pool exhausted") - // ErrPoolClosed connection pool is closed. - ErrPoolClosed = errors.New("container/pool closed") - - // nowFunc returns the current time; it's overridden in tests. - nowFunc = time.Now -) - -// Config is the pool configuration struct. -type Config struct { - // Active number of items allocated by the pool at a given time. - // When zero, there is no limit on the number of items in the pool. - Active int - // Idle number of idle items in the pool. - Idle int - // Close items after remaining item for this duration. If the value - // is zero, then item items are not closed. Applications should set - // the timeout to a value less than the server's timeout. - IdleTimeout xtime.Duration - // If WaitTimeout is set and the pool is at the Active limit, then Get() waits WatiTimeout - // until a item to be returned to the pool before returning. - WaitTimeout xtime.Duration - // If WaitTimeout is not set, then Wait effects. - // if Wait is set true, then wait until ctx timeout, or default flase and return directly. - Wait bool -} - -type item struct { - createdAt time.Time - c io.Closer -} - -func (i *item) expired(timeout time.Duration) bool { - if timeout <= 0 { - return false - } - return i.createdAt.Add(timeout).Before(nowFunc()) -} - -func (i *item) close() error { - return i.c.Close() -} - -// Pool interface. -type Pool interface { - Get(ctx context.Context) (io.Closer, error) - Put(ctx context.Context, c io.Closer, forceClose bool) error - Close() error -} diff --git a/pkg/container/pool/slice.go b/pkg/container/pool/slice.go deleted file mode 100644 index 512ece39b..000000000 --- a/pkg/container/pool/slice.go +++ /dev/null @@ -1,418 +0,0 @@ -package pool - -import ( - "context" - "io" - "sync" - "time" -) - -var _ Pool = &Slice{} - -// Slice . -type Slice struct { - // New is an application supplied function for creating and configuring a - // item. - // - // The item returned from new must not be in a special state - // (subscribed to pubsub channel, transaction started, ...). - New func(ctx context.Context) (io.Closer, error) - stop func() // stop cancels the item opener. - - // mu protects fields defined below. - mu sync.Mutex - freeItem []*item - itemRequests map[uint64]chan item - nextRequest uint64 // Next key to use in itemRequests. - active int // number of opened and pending open items - // Used to signal the need for new items - // a goroutine running itemOpener() reads on this chan and - // maybeOpenNewItems sends on the chan (one send per needed item) - // It is closed during db.Close(). The close tells the itemOpener - // goroutine to exit. - openerCh chan struct{} - closed bool - cleanerCh chan struct{} - - // Config pool configuration - conf *Config -} - -// NewSlice creates a new pool. -func NewSlice(c *Config) *Slice { - // check Config - if c == nil || c.Active < c.Idle { - panic("config nil or Idle Must <= Active") - } - ctx, cancel := context.WithCancel(context.Background()) - // new pool - p := &Slice{ - conf: c, - stop: cancel, - itemRequests: make(map[uint64]chan item), - openerCh: make(chan struct{}, 1000000), - } - p.startCleanerLocked(time.Duration(c.IdleTimeout)) - - go p.itemOpener(ctx) - return p -} - -// Reload reload config. -func (p *Slice) Reload(c *Config) error { - p.mu.Lock() - p.setActive(c.Active) - p.setIdle(c.Idle) - p.conf = c - p.mu.Unlock() - return nil -} - -// Get returns a newly-opened or cached *item. -func (p *Slice) Get(ctx context.Context) (io.Closer, error) { - p.mu.Lock() - if p.closed { - p.mu.Unlock() - return nil, ErrPoolClosed - } - idleTimeout := time.Duration(p.conf.IdleTimeout) - // Prefer a free item, if possible. - numFree := len(p.freeItem) - for numFree > 0 { - i := p.freeItem[0] - copy(p.freeItem, p.freeItem[1:]) - p.freeItem = p.freeItem[:numFree-1] - p.mu.Unlock() - if i.expired(idleTimeout) { - i.close() - p.mu.Lock() - p.release() - } else { - return i.c, nil - } - numFree = len(p.freeItem) - } - - // Out of free items or we were asked not to use one. If we're not - // allowed to open any more items, make a request and wait. - if p.conf.Active > 0 && p.active >= p.conf.Active { - // check WaitTimeout and return directly - if p.conf.WaitTimeout == 0 && !p.conf.Wait { - p.mu.Unlock() - return nil, ErrPoolExhausted - } - // Make the item channel. It's buffered so that the - // itemOpener doesn't block while waiting for the req to be read. - req := make(chan item, 1) - reqKey := p.nextRequestKeyLocked() - p.itemRequests[reqKey] = req - wt := p.conf.WaitTimeout - p.mu.Unlock() - - // reset context timeout - if wt > 0 { - var cancel func() - _, ctx, cancel = wt.Shrink(ctx) - defer cancel() - } - // Timeout the item request with the context. - select { - case <-ctx.Done(): - // Remove the item request and ensure no value has been sent - // on it after removing. - p.mu.Lock() - delete(p.itemRequests, reqKey) - p.mu.Unlock() - return nil, ctx.Err() - case ret, ok := <-req: - if !ok { - return nil, ErrPoolClosed - } - if ret.expired(idleTimeout) { - ret.close() - p.mu.Lock() - p.release() - } else { - return ret.c, nil - } - } - } - - p.active++ // optimistically - p.mu.Unlock() - c, err := p.New(ctx) - if err != nil { - p.mu.Lock() - p.release() - p.mu.Unlock() - return nil, err - } - return c, nil -} - -// Put adds a item to the p's free pool. -// err is optionally the last error that occurred on this item. -func (p *Slice) Put(ctx context.Context, c io.Closer, forceClose bool) error { - p.mu.Lock() - defer p.mu.Unlock() - if forceClose { - p.release() - return c.Close() - } - added := p.putItemLocked(c) - if !added { - p.active-- - return c.Close() - } - return nil -} - -// Satisfy a item or put the item in the idle pool and return true -// or return false. -// putItemLocked will satisfy a item if there is one, or it will -// return the *item to the freeItem list if err == nil and the idle -// item limit will not be exceeded. -// If err != nil, the value of i is ignored. -// If err == nil, then i must not equal nil. -// If a item was fulfilled or the *item was placed in the -// freeItem list, then true is returned, otherwise false is returned. -func (p *Slice) putItemLocked(c io.Closer) bool { - if p.closed { - return false - } - if p.conf.Active > 0 && p.active > p.conf.Active { - return false - } - i := item{ - c: c, - createdAt: nowFunc(), - } - if l := len(p.itemRequests); l > 0 { - var req chan item - var reqKey uint64 - for reqKey, req = range p.itemRequests { - break - } - delete(p.itemRequests, reqKey) // Remove from pending requests. - req <- i - return true - } else if !p.closed && p.maxIdleItemsLocked() > len(p.freeItem) { - p.freeItem = append(p.freeItem, &i) - return true - } - return false -} - -// Runs in a separate goroutine, opens new item when requested. -func (p *Slice) itemOpener(ctx context.Context) { - for { - select { - case <-ctx.Done(): - return - case <-p.openerCh: - p.openNewItem(ctx) - } - } -} - -func (p *Slice) maybeOpenNewItems() { - numRequests := len(p.itemRequests) - if p.conf.Active > 0 { - numCanOpen := p.conf.Active - p.active - if numRequests > numCanOpen { - numRequests = numCanOpen - } - } - for numRequests > 0 { - p.active++ // optimistically - numRequests-- - if p.closed { - return - } - p.openerCh <- struct{}{} - } -} - -// openNewItem one new item -func (p *Slice) openNewItem(ctx context.Context) { - // maybeOpenNewConnctions has already executed p.active++ before it sent - // on p.openerCh. This function must execute p.active-- if the - // item fails or is closed before returning. - c, err := p.New(ctx) - p.mu.Lock() - defer p.mu.Unlock() - if err != nil { - p.release() - return - } - if !p.putItemLocked(c) { - p.active-- - c.Close() - } -} - -// setIdle sets the maximum number of items in the idle -// item pool. -// -// If MaxOpenConns is greater than 0 but less than the new IdleConns -// then the new IdleConns will be reduced to match the MaxOpenConns limit -// -// If n <= 0, no idle items are retained. -func (p *Slice) setIdle(n int) { - if n > 0 { - p.conf.Idle = n - } else { - // No idle items. - p.conf.Idle = -1 - } - // Make sure maxIdle doesn't exceed maxOpen - if p.conf.Active > 0 && p.maxIdleItemsLocked() > p.conf.Active { - p.conf.Idle = p.conf.Active - } - var closing []*item - idleCount := len(p.freeItem) - maxIdle := p.maxIdleItemsLocked() - if idleCount > maxIdle { - closing = p.freeItem[maxIdle:] - p.freeItem = p.freeItem[:maxIdle] - } - for _, c := range closing { - c.close() - } -} - -// setActive sets the maximum number of open items to the database. -// -// If IdleConns is greater than 0 and the new MaxOpenConns is less than -// IdleConns, then IdleConns will be reduced to match the new -// MaxOpenConns limit -// -// If n <= 0, then there is no limit on the number of open items. -// The default is 0 (unlimited). -func (p *Slice) setActive(n int) { - p.conf.Active = n - if n < 0 { - p.conf.Active = 0 - } - syncIdle := p.conf.Active > 0 && p.maxIdleItemsLocked() > p.conf.Active - if syncIdle { - p.setIdle(n) - } -} - -// startCleanerLocked starts itemCleaner if needed. -func (p *Slice) startCleanerLocked(d time.Duration) { - if d <= 0 { - // if set 0, staleCleaner() will return directly - return - } - if d < time.Duration(p.conf.IdleTimeout) && p.cleanerCh != nil { - select { - case p.cleanerCh <- struct{}{}: - default: - } - } - // run only one, clean stale items. - if p.cleanerCh == nil { - p.cleanerCh = make(chan struct{}, 1) - go p.staleCleaner(time.Duration(p.conf.IdleTimeout)) - } -} - -func (p *Slice) staleCleaner(d time.Duration) { - const minInterval = 100 * time.Millisecond - - if d < minInterval { - d = minInterval - } - t := time.NewTimer(d) - - for { - select { - case <-t.C: - case <-p.cleanerCh: // maxLifetime was changed or db was closed. - } - p.mu.Lock() - d = time.Duration(p.conf.IdleTimeout) - if p.closed || d <= 0 { - p.mu.Unlock() - return - } - - expiredSince := nowFunc().Add(-d) - var closing []*item - for i := 0; i < len(p.freeItem); i++ { - c := p.freeItem[i] - if c.createdAt.Before(expiredSince) { - closing = append(closing, c) - p.active-- - last := len(p.freeItem) - 1 - p.freeItem[i] = p.freeItem[last] - p.freeItem[last] = nil - p.freeItem = p.freeItem[:last] - i-- - } - } - p.mu.Unlock() - - for _, c := range closing { - c.close() - } - - if d < minInterval { - d = minInterval - } - t.Reset(d) - } -} - -// nextRequestKeyLocked returns the next item request key. -// It is assumed that nextRequest will not overflow. -func (p *Slice) nextRequestKeyLocked() uint64 { - next := p.nextRequest - p.nextRequest++ - return next -} - -const defaultIdleItems = 2 - -func (p *Slice) maxIdleItemsLocked() int { - n := p.conf.Idle - switch { - case n == 0: - return defaultIdleItems - case n < 0: - return 0 - default: - return n - } -} - -func (p *Slice) release() { - p.active-- - p.maybeOpenNewItems() -} - -// Close close pool. -func (p *Slice) Close() error { - p.mu.Lock() - if p.closed { - p.mu.Unlock() - return nil - } - if p.cleanerCh != nil { - close(p.cleanerCh) - } - var err error - for _, i := range p.freeItem { - i.close() - } - p.freeItem = nil - p.closed = true - for _, req := range p.itemRequests { - close(req) - } - p.mu.Unlock() - p.stop() - return err -} diff --git a/pkg/container/pool/slice_test.go b/pkg/container/pool/slice_test.go deleted file mode 100644 index b7939203e..000000000 --- a/pkg/container/pool/slice_test.go +++ /dev/null @@ -1,350 +0,0 @@ -package pool - -import ( - "context" - "io" - "testing" - "time" - - xtime "github.com/go-kratos/kratos/pkg/time" - - "github.com/stretchr/testify/assert" -) - -type closer struct { -} - -func (c *closer) Close() error { - return nil -} - -type connection struct { - c io.Closer - pool Pool -} - -func (c *connection) HandleQuick() { - // time.Sleep(1 * time.Millisecond) -} - -func (c *connection) HandleNormal() { - time.Sleep(20 * time.Millisecond) -} - -func (c *connection) HandleSlow() { - time.Sleep(500 * time.Millisecond) -} - -func (c *connection) Close() { - c.pool.Put(context.Background(), c.c, false) -} - -func TestSliceGetPut(t *testing.T) { - // new pool - config := &Config{ - Active: 1, - Idle: 1, - IdleTimeout: xtime.Duration(90 * time.Second), - WaitTimeout: xtime.Duration(10 * time.Millisecond), - Wait: false, - } - pool := NewSlice(config) - pool.New = func(ctx context.Context) (io.Closer, error) { - return &closer{}, nil - } - - // test Get Put - conn, err := pool.Get(context.TODO()) - assert.Nil(t, err) - c1 := connection{pool: pool, c: conn} - c1.HandleNormal() - c1.Close() -} - -func TestSlicePut(t *testing.T) { - var id = 0 - type connID struct { - io.Closer - id int - } - config := &Config{ - Active: 1, - Idle: 1, - IdleTimeout: xtime.Duration(1 * time.Second), - // WaitTimeout: xtime.Duration(10 * time.Millisecond), - Wait: false, - } - pool := NewSlice(config) - pool.New = func(ctx context.Context) (io.Closer, error) { - id = id + 1 - return &connID{id: id, Closer: &closer{}}, nil - } - // test Put(ctx, conn, true) - conn, err := pool.Get(context.TODO()) - assert.Nil(t, err) - conn1 := conn.(*connID) - // Put(ctx, conn, true) drop the connection. - pool.Put(context.TODO(), conn, true) - conn, err = pool.Get(context.TODO()) - assert.Nil(t, err) - conn2 := conn.(*connID) - assert.NotEqual(t, conn1.id, conn2.id) -} - -func TestSliceIdleTimeout(t *testing.T) { - var id = 0 - type connID struct { - io.Closer - id int - } - config := &Config{ - Active: 1, - Idle: 1, - // conn timeout - IdleTimeout: xtime.Duration(1 * time.Millisecond), - } - pool := NewSlice(config) - pool.New = func(ctx context.Context) (io.Closer, error) { - id = id + 1 - return &connID{id: id, Closer: &closer{}}, nil - } - // test Put(ctx, conn, true) - conn, err := pool.Get(context.TODO()) - assert.Nil(t, err) - conn1 := conn.(*connID) - // Put(ctx, conn, true) drop the connection. - pool.Put(context.TODO(), conn, false) - time.Sleep(5 * time.Millisecond) - // idletimeout and get new conn - conn, err = pool.Get(context.TODO()) - assert.Nil(t, err) - conn2 := conn.(*connID) - assert.NotEqual(t, conn1.id, conn2.id) -} - -func TestSliceContextTimeout(t *testing.T) { - // new pool - config := &Config{ - Active: 1, - Idle: 1, - IdleTimeout: xtime.Duration(90 * time.Second), - WaitTimeout: xtime.Duration(10 * time.Millisecond), - Wait: false, - } - pool := NewSlice(config) - pool.New = func(ctx context.Context) (io.Closer, error) { - return &closer{}, nil - } - // test context timeout - ctx, cancel := context.WithTimeout(context.TODO(), 100*time.Millisecond) - defer cancel() - conn, err := pool.Get(ctx) - assert.Nil(t, err) - _, err = pool.Get(ctx) - // context timeout error - assert.NotNil(t, err) - pool.Put(context.TODO(), conn, false) - _, err = pool.Get(ctx) - assert.Nil(t, err) -} - -func TestSlicePoolExhausted(t *testing.T) { - // test pool exhausted - config := &Config{ - Active: 1, - Idle: 1, - IdleTimeout: xtime.Duration(90 * time.Second), - // WaitTimeout: xtime.Duration(10 * time.Millisecond), - Wait: false, - } - pool := NewSlice(config) - pool.New = func(ctx context.Context) (io.Closer, error) { - return &closer{}, nil - } - - ctx, cancel := context.WithTimeout(context.TODO(), 100*time.Millisecond) - defer cancel() - conn, err := pool.Get(context.TODO()) - assert.Nil(t, err) - _, err = pool.Get(ctx) - // config active == 1, so no available conns make connection exhausted. - assert.NotNil(t, err) - pool.Put(context.TODO(), conn, false) - _, err = pool.Get(ctx) - assert.Nil(t, err) -} - -func TestSliceStaleClean(t *testing.T) { - var id = 0 - type connID struct { - io.Closer - id int - } - config := &Config{ - Active: 1, - Idle: 1, - IdleTimeout: xtime.Duration(1 * time.Second), - // WaitTimeout: xtime.Duration(10 * time.Millisecond), - Wait: false, - } - pool := NewList(config) - pool.New = func(ctx context.Context) (io.Closer, error) { - id = id + 1 - return &connID{id: id, Closer: &closer{}}, nil - } - conn, err := pool.Get(context.TODO()) - assert.Nil(t, err) - conn1 := conn.(*connID) - pool.Put(context.TODO(), conn, false) - conn, err = pool.Get(context.TODO()) - assert.Nil(t, err) - conn2 := conn.(*connID) - assert.Equal(t, conn1.id, conn2.id) - pool.Put(context.TODO(), conn, false) - // sleep more than idleTimeout - time.Sleep(2 * time.Second) - conn, err = pool.Get(context.TODO()) - assert.Nil(t, err) - conn3 := conn.(*connID) - assert.NotEqual(t, conn1.id, conn3.id) -} - -func BenchmarkSlice1(b *testing.B) { - config := &Config{ - Active: 30, - Idle: 30, - IdleTimeout: xtime.Duration(90 * time.Second), - WaitTimeout: xtime.Duration(10 * time.Millisecond), - Wait: false, - } - pool := NewSlice(config) - pool.New = func(ctx context.Context) (io.Closer, error) { - return &closer{}, nil - } - - b.ResetTimer() - b.RunParallel(func(pb *testing.PB) { - for pb.Next() { - conn, err := pool.Get(context.TODO()) - if err != nil { - b.Error(err) - continue - } - c1 := connection{pool: pool, c: conn} - c1.HandleQuick() - c1.Close() - } - }) -} - -func BenchmarkSlice2(b *testing.B) { - config := &Config{ - Active: 30, - Idle: 30, - IdleTimeout: xtime.Duration(90 * time.Second), - WaitTimeout: xtime.Duration(10 * time.Millisecond), - Wait: false, - } - pool := NewSlice(config) - pool.New = func(ctx context.Context) (io.Closer, error) { - return &closer{}, nil - } - - b.ResetTimer() - b.RunParallel(func(pb *testing.PB) { - for pb.Next() { - conn, err := pool.Get(context.TODO()) - if err != nil { - b.Error(err) - continue - } - c1 := connection{pool: pool, c: conn} - c1.HandleNormal() - c1.Close() - } - }) -} - -func BenchmarkSlice3(b *testing.B) { - config := &Config{ - Active: 30, - Idle: 30, - IdleTimeout: xtime.Duration(90 * time.Second), - WaitTimeout: xtime.Duration(10 * time.Millisecond), - Wait: false, - } - pool := NewSlice(config) - pool.New = func(ctx context.Context) (io.Closer, error) { - return &closer{}, nil - } - - b.ResetTimer() - b.RunParallel(func(pb *testing.PB) { - for pb.Next() { - conn, err := pool.Get(context.TODO()) - if err != nil { - b.Error(err) - continue - } - c1 := connection{pool: pool, c: conn} - c1.HandleSlow() - c1.Close() - } - }) -} - -func BenchmarkSlice4(b *testing.B) { - config := &Config{ - Active: 30, - Idle: 30, - IdleTimeout: xtime.Duration(90 * time.Second), - // WaitTimeout: xtime.Duration(10 * time.Millisecond), - Wait: false, - } - pool := NewSlice(config) - pool.New = func(ctx context.Context) (io.Closer, error) { - return &closer{}, nil - } - - b.ResetTimer() - b.RunParallel(func(pb *testing.PB) { - for pb.Next() { - conn, err := pool.Get(context.TODO()) - if err != nil { - b.Error(err) - continue - } - c1 := connection{pool: pool, c: conn} - c1.HandleSlow() - c1.Close() - } - }) -} - -func BenchmarkSlice5(b *testing.B) { - config := &Config{ - Active: 30, - Idle: 30, - IdleTimeout: xtime.Duration(90 * time.Second), - // WaitTimeout: xtime.Duration(10 * time.Millisecond), - Wait: true, - } - pool := NewSlice(config) - pool.New = func(ctx context.Context) (io.Closer, error) { - return &closer{}, nil - } - - b.ResetTimer() - b.RunParallel(func(pb *testing.PB) { - for pb.Next() { - conn, err := pool.Get(context.TODO()) - if err != nil { - b.Error(err) - continue - } - c1 := connection{pool: pool, c: conn} - c1.HandleSlow() - c1.Close() - } - }) -} diff --git a/pkg/container/queue/aqm/README.md b/pkg/container/queue/aqm/README.md deleted file mode 100644 index 273de9b8d..000000000 --- a/pkg/container/queue/aqm/README.md +++ /dev/null @@ -1,5 +0,0 @@ -# aqm - -## 项目简介 - -队列管理算法 diff --git a/pkg/container/queue/aqm/codel.go b/pkg/container/queue/aqm/codel.go deleted file mode 100644 index 0b0938bfc..000000000 --- a/pkg/container/queue/aqm/codel.go +++ /dev/null @@ -1,201 +0,0 @@ -package aqm - -import ( - "context" - "math" - "sync" - "time" - - "github.com/go-kratos/kratos/pkg/ecode" -) - -// Config codel config. -type Config struct { - Target int64 // target queue delay (default 20 ms). - Internal int64 // sliding minimum time window width (default 500 ms) - MaxOutstanding int64 //max num of concurrent acquires -} - -// Stat is the Statistics of codel. -type Stat struct { - Dropping bool - FaTime int64 - DropNext int64 - Packets int -} - -type packet struct { - ch chan bool - ts int64 -} - -var defaultConf = &Config{ - Target: 50, - Internal: 500, - MaxOutstanding: 40, -} - -// Queue queue is CoDel req buffer queue. -type Queue struct { - pool sync.Pool - packets chan packet - - mux sync.RWMutex - conf *Config - count int64 - dropping bool // Equal to 1 if in drop state - faTime int64 // Time when we'll declare we're above target (0 if below) - dropNext int64 // Packets dropped since going into drop state - outstanding int64 -} - -// Default new a default codel queue. -func Default() *Queue { - return New(defaultConf) -} - -// New new codel queue. -func New(conf *Config) *Queue { - if conf == nil { - conf = defaultConf - } - q := &Queue{ - packets: make(chan packet, 2048), - conf: conf, - } - q.pool.New = func() interface{} { - return make(chan bool) - } - return q -} - -// Reload set queue config. -func (q *Queue) Reload(c *Config) { - if c == nil || c.Internal <= 0 || c.Target <= 0 || c.MaxOutstanding <= 0 { - return - } - // TODO codel queue size - q.mux.Lock() - q.conf = c - q.mux.Unlock() -} - -// Stat return the statistics of codel -func (q *Queue) Stat() Stat { - q.mux.Lock() - defer q.mux.Unlock() - return Stat{ - Dropping: q.dropping, - FaTime: q.faTime, - DropNext: q.dropNext, - Packets: len(q.packets), - } -} - -// Push req into CoDel request buffer queue. -// if return error is nil,the caller must call q.Done() after finish request handling -func (q *Queue) Push(ctx context.Context) (err error) { - q.mux.Lock() - if q.outstanding < q.conf.MaxOutstanding && len(q.packets) == 0 { - q.outstanding ++ - q.mux.Unlock() - return - } - q.mux.Unlock() - r := packet{ - ch: q.pool.Get().(chan bool), - ts: time.Now().UnixNano() / int64(time.Millisecond), - } - select { - case q.packets <- r: - default: - err = ecode.LimitExceed - q.pool.Put(r.ch) - } - if err == nil { - select { - case drop := <-r.ch: - if drop { - err = ecode.LimitExceed - } - q.pool.Put(r.ch) - case <-ctx.Done(): - err = ecode.Deadline - } - } - return -} - -// Pop req from CoDel request buffer queue. -func (q *Queue) Pop() { - q.mux.Lock() - q.outstanding -- - if q.outstanding < 0 { - q.outstanding = 0 - q.mux.Unlock() - return - } - defer q.mux.Unlock() - for { - select { - case p := <-q.packets: - drop := q.judge(p) - select { - case p.ch <- drop: - if !drop { - return - } - default: - q.pool.Put(p.ch) - } - default: - return - } - } -} - -func (q *Queue) controlLaw(now int64) int64 { - q.dropNext = now + int64(float64(q.conf.Internal)/math.Sqrt(float64(q.count))) - return q.dropNext -} - -// judge decide if the packet should drop or not. -func (q *Queue) judge(p packet) (drop bool) { - now := time.Now().UnixNano() / int64(time.Millisecond) - sojurn := now - p.ts - if sojurn < q.conf.Target { - q.faTime = 0 - } else if q.faTime == 0 { - q.faTime = now + q.conf.Internal - } else if now >= q.faTime { - drop = true - } - if q.dropping { - if !drop { - // sojourn time below target - leave dropping state - q.dropping = false - } else if now > q.dropNext { - q.count++ - q.dropNext = q.controlLaw(q.dropNext) - return - } - } else if drop && (now-q.dropNext < q.conf.Internal || now-q.faTime >= q.conf.Internal) { - q.dropping = true - // If we're in a drop cycle, the drop rate that controlled the queue - // on the last cycle is a good starting point to control it now. - if now-q.dropNext < q.conf.Internal { - if q.count > 2 { - q.count = q.count - 2 - } else { - q.count = 1 - } - } else { - q.count = 1 - } - q.dropNext = q.controlLaw(now) - return - } - q.outstanding ++ - drop = false - return -} diff --git a/pkg/container/queue/aqm/codel_test.go b/pkg/container/queue/aqm/codel_test.go deleted file mode 100644 index 7e2fdb819..000000000 --- a/pkg/container/queue/aqm/codel_test.go +++ /dev/null @@ -1,101 +0,0 @@ -package aqm - -import ( - "context" - "fmt" - "sync" - "sync/atomic" - "testing" - "time" - - "github.com/go-kratos/kratos/pkg/ecode" -) - -var testConf = &Config{ - Target: 20, - Internal: 500, - MaxOutstanding: 20, -} - -var qps = time.Microsecond * 2000 - -func TestCoDel1200(t *testing.T) { - q := New(testConf) - drop := new(int64) - tm := new(int64) - accept := new(int64) - delay := time.Millisecond * 3000 - testPush(q, qps, delay, drop, tm, accept) - fmt.Printf("qps %v process time %v drop %d timeout %d accept %d \n", int64(time.Second/qps), delay, *drop, *tm, *accept) - time.Sleep(time.Second) -} - -func TestCoDel200(t *testing.T) { - q := New(testConf) - drop := new(int64) - tm := new(int64) - accept := new(int64) - delay := time.Millisecond * 2000 - testPush(q, qps, delay, drop, tm, accept) - fmt.Printf("qps %v process time %v drop %d timeout %d accept %d \n", int64(time.Second/qps), delay, *drop, *tm, *accept) - time.Sleep(time.Second) -} - -func TestCoDel100(t *testing.T) { - q := New(testConf) - drop := new(int64) - tm := new(int64) - accept := new(int64) - delay := time.Millisecond * 1000 - testPush(q, qps, delay, drop, tm, accept) - fmt.Printf("qps %v process time %v drop %d timeout %d accept %d \n", int64(time.Second/qps), delay, *drop, *tm, *accept) -} - -func TestCoDel50(t *testing.T) { - q := New(testConf) - drop := new(int64) - tm := new(int64) - accept := new(int64) - delay := time.Millisecond * 50 - testPush(q, qps, delay, drop, tm, accept) - fmt.Printf("qps %v process time %v drop %d timeout %d accept %d \n", int64(time.Second/qps), delay, *drop, *tm, *accept) -} - -func testPush(q *Queue, sleep time.Duration, delay time.Duration, drop *int64, tm *int64, accept *int64) { - var group sync.WaitGroup - for i := 0; i < 5000; i++ { - time.Sleep(sleep) - group.Add(1) - go func() { - defer group.Done() - ctx, cancel := context.WithDeadline(context.Background(), time.Now().Add(time.Millisecond*1000)) - defer cancel() - if err := q.Push(ctx); err != nil { - if err == ecode.LimitExceed { - atomic.AddInt64(drop, 1) - } else { - atomic.AddInt64(tm, 1) - } - } else { - atomic.AddInt64(accept, 1) - time.Sleep(delay) - q.Pop() - } - }() - } - group.Wait() -} - -func BenchmarkAQM(b *testing.B) { - q := Default() - b.RunParallel(func(p *testing.PB) { - for p.Next() { - ctx, cancel := context.WithDeadline(context.Background(), time.Now().Add(time.Millisecond*5)) - err := q.Push(ctx) - if err == nil { - q.Pop() - } - cancel() - } - }) -} diff --git a/pkg/database/hbase/README.md b/pkg/database/hbase/README.md deleted file mode 100644 index 8e1280821..000000000 --- a/pkg/database/hbase/README.md +++ /dev/null @@ -1,40 +0,0 @@ -### database/hbase - -### 项目简介 - -Hbase Client,进行封装加入了链路追踪和统计。 - -### usage -```go -package main - -import ( - "context" - "fmt" - - "github.com/go-kratos/kratos/pkg/database/hbase" -) - -func main() { - config := &hbase.Config{Zookeeper: &hbase.ZKConfig{Addrs: []string{"localhost"}}} - client := hbase.NewClient(config) - - values := map[string]map[string][]byte{"name": {"firstname": []byte("hello"), "lastname": []byte("world")}} - ctx := context.Background() - - _, err := client.PutStr(ctx, "user", "user1", values) - if err != nil { - panic(err) - } - - result, err := client.GetStr(ctx, "user", "user1") - if err != nil { - panic(err) - } - fmt.Printf("%v", result) -} -``` - -##### 依赖包 - -1.[gohbase](https://github.com/tsuna/gohbase) diff --git a/pkg/database/hbase/config.go b/pkg/database/hbase/config.go deleted file mode 100644 index 8b24b0862..000000000 --- a/pkg/database/hbase/config.go +++ /dev/null @@ -1,23 +0,0 @@ -package hbase - -import ( - xtime "github.com/go-kratos/kratos/pkg/time" -) - -// ZKConfig Server&Client settings. -type ZKConfig struct { - Root string - Addrs []string - Timeout xtime.Duration -} - -// Config hbase config -type Config struct { - Zookeeper *ZKConfig - RPCQueueSize int - FlushInterval xtime.Duration - EffectiveUser string - RegionLookupTimeout xtime.Duration - RegionReadTimeout xtime.Duration - TestRowKey string -} diff --git a/pkg/database/hbase/hbase.go b/pkg/database/hbase/hbase.go deleted file mode 100644 index 8534ad40d..000000000 --- a/pkg/database/hbase/hbase.go +++ /dev/null @@ -1,297 +0,0 @@ -package hbase - -import ( - "context" - "io" - "strings" - "time" - - "github.com/tsuna/gohbase" - "github.com/tsuna/gohbase/hrpc" - - "github.com/go-kratos/kratos/pkg/log" -) - -// HookFunc hook function call before every method and hook return function will call after finish. -type HookFunc func(ctx context.Context, call hrpc.Call, customName string) func(err error) - -// Client hbase client. -type Client struct { - hc gohbase.Client - addr string - config *Config - hooks []HookFunc -} - -// AddHook add hook function. -func (c *Client) AddHook(hookFn HookFunc) { - c.hooks = append(c.hooks, hookFn) -} - -func (c *Client) invokeHook(ctx context.Context, call hrpc.Call, customName string) func(error) { - finishHooks := make([]func(error), 0, len(c.hooks)) - for _, fn := range c.hooks { - finishHooks = append(finishHooks, fn(ctx, call, customName)) - } - return func(err error) { - for _, fn := range finishHooks { - fn(err) - } - } -} - -// NewClient new a hbase client. -func NewClient(config *Config, options ...gohbase.Option) *Client { - rawcli := NewRawClient(config, options...) - rawcli.AddHook(NewSlowLogHook(250 * time.Millisecond)) - rawcli.AddHook(MetricsHook(config)) - rawcli.AddHook(TraceHook("database/hbase", strings.Join(config.Zookeeper.Addrs, ","))) - return rawcli -} - -// NewRawClient new a hbase client without prometheus metrics and dapper trace hook. -func NewRawClient(config *Config, options ...gohbase.Option) *Client { - zkquorum := strings.Join(config.Zookeeper.Addrs, ",") - if config.Zookeeper.Root != "" { - options = append(options, gohbase.ZookeeperRoot(config.Zookeeper.Root)) - } - if config.Zookeeper.Timeout != 0 { - options = append(options, gohbase.ZookeeperTimeout(time.Duration(config.Zookeeper.Timeout))) - } - - if config.RPCQueueSize != 0 { - log.Warn("RPCQueueSize configuration be ignored") - } - // force RpcQueueSize = 1, don't change it !!! it has reason (゜-゜)つロ - options = append(options, gohbase.RpcQueueSize(1)) - - if config.FlushInterval != 0 { - options = append(options, gohbase.FlushInterval(time.Duration(config.FlushInterval))) - } - if config.EffectiveUser != "" { - options = append(options, gohbase.EffectiveUser(config.EffectiveUser)) - } - if config.RegionLookupTimeout != 0 { - options = append(options, gohbase.RegionLookupTimeout(time.Duration(config.RegionLookupTimeout))) - } - if config.RegionReadTimeout != 0 { - options = append(options, gohbase.RegionReadTimeout(time.Duration(config.RegionReadTimeout))) - } - hc := gohbase.NewClient(zkquorum, options...) - return &Client{ - hc: hc, - addr: zkquorum, - config: config, - } -} - -// ScanAll do scan command and return all result -// NOTE: if err != nil the results is safe for range operate even not result found -func (c *Client) ScanAll(ctx context.Context, table []byte, options ...func(hrpc.Call) error) (results []*hrpc.Result, err error) { - cursor, err := c.Scan(ctx, table, options...) - if err != nil { - return nil, err - } - for { - result, err := cursor.Next() - if err != nil { - if err == io.EOF { - break - } - return nil, err - } - results = append(results, result) - } - return results, nil -} - -type scanTrace struct { - hrpc.Scanner - finishHook func(error) -} - -func (s *scanTrace) Next() (*hrpc.Result, error) { - result, err := s.Scanner.Next() - if err != nil { - s.finishHook(err) - } - return result, err -} - -func (s *scanTrace) Close() error { - err := s.Scanner.Close() - s.finishHook(err) - return err -} - -// Scan do a scan command. -func (c *Client) Scan(ctx context.Context, table []byte, options ...func(hrpc.Call) error) (scanner hrpc.Scanner, err error) { - var scan *hrpc.Scan - scan, err = hrpc.NewScan(ctx, table, options...) - if err != nil { - return nil, err - } - st := &scanTrace{} - st.finishHook = c.invokeHook(ctx, scan, "Scan") - st.Scanner = c.hc.Scan(scan) - return st, nil -} - -// ScanStr scan string -func (c *Client) ScanStr(ctx context.Context, table string, options ...func(hrpc.Call) error) (hrpc.Scanner, error) { - return c.Scan(ctx, []byte(table), options...) -} - -// ScanStrAll scan string -// NOTE: if err != nil the results is safe for range operate even not result found -func (c *Client) ScanStrAll(ctx context.Context, table string, options ...func(hrpc.Call) error) ([]*hrpc.Result, error) { - return c.ScanAll(ctx, []byte(table), options...) -} - -// ScanRange get a scanner for the given table and key range. -// The range is half-open, i.e. [startRow; stopRow[ -- stopRow is not -// included in the range. -func (c *Client) ScanRange(ctx context.Context, table, startRow, stopRow []byte, options ...func(hrpc.Call) error) (scanner hrpc.Scanner, err error) { - var scan *hrpc.Scan - scan, err = hrpc.NewScanRange(ctx, table, startRow, stopRow, options...) - if err != nil { - return nil, err - } - st := &scanTrace{} - st.finishHook = c.invokeHook(ctx, scan, "ScanRange") - st.Scanner = c.hc.Scan(scan) - return st, nil -} - -// ScanRangeStr get a scanner for the given table and key range. -// The range is half-open, i.e. [startRow; stopRow[ -- stopRow is not -// included in the range. -func (c *Client) ScanRangeStr(ctx context.Context, table, startRow, stopRow string, options ...func(hrpc.Call) error) (hrpc.Scanner, error) { - return c.ScanRange(ctx, []byte(table), []byte(startRow), []byte(stopRow), options...) -} - -// Get get result for the given table and row key. -// NOTE: if err != nil then result != nil, if result not exists result.Cells length is 0 -func (c *Client) Get(ctx context.Context, table, key []byte, options ...func(hrpc.Call) error) (result *hrpc.Result, err error) { - var get *hrpc.Get - get, err = hrpc.NewGet(ctx, table, key, options...) - if err != nil { - return nil, err - } - - finishHook := c.invokeHook(ctx, get, "GET") - result, err = c.hc.Get(get) - finishHook(err) - return -} - -// GetStr do a get command. -// NOTE: if err != nil then result != nil, if result not exists result.Cells length is 0 -func (c *Client) GetStr(ctx context.Context, table, key string, options ...func(hrpc.Call) error) (result *hrpc.Result, err error) { - return c.Get(ctx, []byte(table), []byte(key), options...) -} - -// PutStr insert the given family-column-values in the given row key of the given table. -func (c *Client) PutStr(ctx context.Context, table string, key string, values map[string]map[string][]byte, options ...func(hrpc.Call) error) (*hrpc.Result, error) { - put, err := hrpc.NewPutStr(ctx, table, key, values, options...) - if err != nil { - return nil, err - } - - finishHook := c.invokeHook(ctx, put, "PUT") - result, err := c.hc.Put(put) - finishHook(err) - return result, err -} - -// Delete is used to perform Delete operations on a single row. -// To delete entire row, values should be nil. -// -// To delete specific families, qualifiers map should be nil: -// map[string]map[string][]byte{ -// "cf1": nil, -// "cf2": nil, -// } -// -// To delete specific qualifiers: -// map[string]map[string][]byte{ -// "cf": map[string][]byte{ -// "q1": nil, -// "q2": nil, -// }, -// } -// -// To delete all versions before and at a timestamp, pass hrpc.Timestamp() option. -// By default all versions will be removed. -// -// To delete only a specific version at a timestamp, pass hrpc.DeleteOneVersion() option -// along with a timestamp. For delete specific qualifiers request, if timestamp is not -// passed, only the latest version will be removed. For delete specific families request, -// the timestamp should be passed or it will have no effect as it's an expensive -// operation to perform. -func (c *Client) Delete(ctx context.Context, table string, key string, values map[string]map[string][]byte, options ...func(hrpc.Call) error) (*hrpc.Result, error) { - del, err := hrpc.NewDelStr(ctx, table, key, values, options...) - if err != nil { - return nil, err - } - - finishHook := c.invokeHook(ctx, del, "Delete") - result, err := c.hc.Delete(del) - finishHook(err) - return result, err -} - -// Append do a append command. -func (c *Client) Append(ctx context.Context, table string, key string, values map[string]map[string][]byte, options ...func(hrpc.Call) error) (*hrpc.Result, error) { - appd, err := hrpc.NewAppStr(ctx, table, key, values, options...) - if err != nil { - return nil, err - } - - finishHook := c.invokeHook(ctx, appd, "Append") - result, err := c.hc.Append(appd) - finishHook(err) - return result, err -} - -// Increment the given values in HBase under the given table and key. -func (c *Client) Increment(ctx context.Context, table string, key string, values map[string]map[string][]byte, options ...func(hrpc.Call) error) (int64, error) { - increment, err := hrpc.NewIncStr(ctx, table, key, values, options...) - if err != nil { - return 0, err - } - finishHook := c.invokeHook(ctx, increment, "Increment") - result, err := c.hc.Increment(increment) - finishHook(err) - return result, err -} - -// IncrementSingle increment the given value by amount in HBase under the given table, key, family and qualifier. -func (c *Client) IncrementSingle(ctx context.Context, table string, key string, family string, qualifier string, amount int64, options ...func(hrpc.Call) error) (int64, error) { - increment, err := hrpc.NewIncStrSingle(ctx, table, key, family, qualifier, amount, options...) - if err != nil { - return 0, err - } - - finishHook := c.invokeHook(ctx, increment, "IncrementSingle") - result, err := c.hc.Increment(increment) - finishHook(err) - return result, err -} - -// Ping ping. -func (c *Client) Ping(ctx context.Context) (err error) { - testRowKey := "test" - if c.config.TestRowKey != "" { - testRowKey = c.config.TestRowKey - } - values := map[string]map[string][]byte{"test": {"test": []byte("test")}} - _, err = c.PutStr(ctx, "test", testRowKey, values) - return -} - -// Close close client. -func (c *Client) Close() error { - c.hc.Close() - return nil -} diff --git a/pkg/database/hbase/metrics.go b/pkg/database/hbase/metrics.go deleted file mode 100644 index ad09331a3..000000000 --- a/pkg/database/hbase/metrics.go +++ /dev/null @@ -1,65 +0,0 @@ -package hbase - -import ( - "context" - "io" - "strings" - "time" - - "github.com/tsuna/gohbase" - "github.com/tsuna/gohbase/hrpc" - - "github.com/go-kratos/kratos/pkg/stat/metric" -) - -const namespace = "hbase_client" - -var ( - _metricReqDur = metric.NewHistogramVec(&metric.HistogramVecOpts{ - Namespace: namespace, - Subsystem: "requests", - Name: "duration_ms", - Help: "hbase client requests duration(ms).", - Labels: []string{"name", "addr", "command"}, - Buckets: []float64{5, 10, 25, 50, 100, 250, 500, 1000, 2500}, - }) - _metricReqErr = metric.NewCounterVec(&metric.CounterVecOpts{ - Namespace: namespace, - Subsystem: "requests", - Name: "error_total", - Help: "mysql client requests error count.", - Labels: []string{"name", "addr", "command", "error"}, - }) -) - -func codeFromErr(err error) string { - code := "unknown_error" - switch err { - case gohbase.ErrClientClosed: - code = "client_closed" - case gohbase.ErrCannotFindRegion: - code = "connot_find_region" - case gohbase.TableNotFound: - code = "table_not_found" - //case gohbase.ErrRegionUnavailable: - // code = "region_unavailable" - } - return code -} - -// MetricsHook if stats is nil use stat.DB as default. -func MetricsHook(config *Config) HookFunc { - return func(ctx context.Context, call hrpc.Call, customName string) func(err error) { - now := time.Now() - if customName == "" { - customName = call.Name() - } - return func(err error) { - durationMs := int64(time.Since(now) / time.Millisecond) - _metricReqDur.Observe(durationMs, strings.Join(config.Zookeeper.Addrs, ","), "", customName) - if err != nil && err != io.EOF { - _metricReqErr.Inc(strings.Join(config.Zookeeper.Addrs, ","), "", customName, codeFromErr(err)) - } - } - } -} diff --git a/pkg/database/hbase/slowlog.go b/pkg/database/hbase/slowlog.go deleted file mode 100644 index 8edd28c4e..000000000 --- a/pkg/database/hbase/slowlog.go +++ /dev/null @@ -1,24 +0,0 @@ -package hbase - -import ( - "context" - "time" - - "github.com/tsuna/gohbase/hrpc" - - "github.com/go-kratos/kratos/pkg/log" -) - -// NewSlowLogHook log slow operation. -func NewSlowLogHook(threshold time.Duration) HookFunc { - return func(ctx context.Context, call hrpc.Call, customName string) func(err error) { - start := time.Now() - return func(error) { - duration := time.Since(start) - if duration < threshold { - return - } - log.Warn("hbase slow log: %s %s %s time: %s", customName, call.Table(), call.Key(), duration) - } - } -} diff --git a/pkg/database/hbase/trace.go b/pkg/database/hbase/trace.go deleted file mode 100644 index e836ce19f..000000000 --- a/pkg/database/hbase/trace.go +++ /dev/null @@ -1,40 +0,0 @@ -package hbase - -import ( - "context" - "io" - - "github.com/tsuna/gohbase/hrpc" - - "github.com/go-kratos/kratos/pkg/net/trace" -) - -// TraceHook create new hbase trace hook. -func TraceHook(component, instance string) HookFunc { - var internalTags []trace.Tag - internalTags = append(internalTags, trace.TagString(trace.TagComponent, component)) - internalTags = append(internalTags, trace.TagString(trace.TagDBInstance, instance)) - internalTags = append(internalTags, trace.TagString(trace.TagPeerService, "hbase")) - internalTags = append(internalTags, trace.TagString(trace.TagSpanKind, "client")) - return func(ctx context.Context, call hrpc.Call, customName string) func(err error) { - noop := func(error) {} - root, ok := trace.FromContext(ctx) - if !ok { - return noop - } - if customName == "" { - customName = call.Name() - } - span := root.Fork("", "Hbase:"+customName) - span.SetTag(internalTags...) - statement := string(call.Table()) + " " + string(call.Key()) - span.SetTag(trace.TagString(trace.TagDBStatement, statement)) - return func(err error) { - if err == io.EOF { - // reset error for trace. - err = nil - } - span.Finish(&err) - } - } -} diff --git a/pkg/database/sql/README.md b/pkg/database/sql/README.md deleted file mode 100644 index d1c36c4dc..000000000 --- a/pkg/database/sql/README.md +++ /dev/null @@ -1,9 +0,0 @@ -#### database/sql - -##### 项目简介 -MySQL数据库驱动,进行封装加入了链路追踪和统计。 - -如果需要SQL级别的超时管理 可以在业务代码里面使用context.WithDeadline实现 推荐超时配置放到application.toml里面 方便热加载 - -##### 依赖包 -1. [Go-MySQL-Driver](https://github.com/go-sql-driver/mysql) diff --git a/pkg/database/sql/metrics.go b/pkg/database/sql/metrics.go deleted file mode 100644 index a4e187d54..000000000 --- a/pkg/database/sql/metrics.go +++ /dev/null @@ -1,37 +0,0 @@ -package sql - -import "github.com/go-kratos/kratos/pkg/stat/metric" - -const namespace = "mysql_client" - -var ( - _metricReqDur = metric.NewHistogramVec(&metric.HistogramVecOpts{ - Namespace: namespace, - Subsystem: "requests", - Name: "duration_ms", - Help: "mysql client requests duration(ms).", - Labels: []string{"name", "addr", "command"}, - Buckets: []float64{5, 10, 25, 50, 100, 250, 500, 1000, 2500}, - }) - _metricReqErr = metric.NewCounterVec(&metric.CounterVecOpts{ - Namespace: namespace, - Subsystem: "requests", - Name: "error_total", - Help: "mysql client requests error count.", - Labels: []string{"name", "addr", "command", "error"}, - }) - _metricConnTotal = metric.NewCounterVec(&metric.CounterVecOpts{ - Namespace: namespace, - Subsystem: "connections", - Name: "total", - Help: "mysql client connections total count.", - Labels: []string{"name", "addr", "state"}, - }) - _metricConnCurrent = metric.NewGaugeVec(&metric.GaugeVecOpts{ - Namespace: namespace, - Subsystem: "connections", - Name: "current", - Help: "mysql client connections current.", - Labels: []string{"name", "addr", "state"}, - }) -) diff --git a/pkg/database/sql/mysql.go b/pkg/database/sql/mysql.go deleted file mode 100644 index 36baf21bf..000000000 --- a/pkg/database/sql/mysql.go +++ /dev/null @@ -1,36 +0,0 @@ -package sql - -import ( - "github.com/go-kratos/kratos/pkg/log" - "github.com/go-kratos/kratos/pkg/net/netutil/breaker" - "github.com/go-kratos/kratos/pkg/time" - - // database driver - _ "github.com/go-sql-driver/mysql" -) - -// Config mysql config. -type Config struct { - DSN string // write data source name. - ReadDSN []string // read data source name. - Active int // pool - Idle int // pool - IdleTimeout time.Duration // connect max life time. - QueryTimeout time.Duration // query sql timeout - ExecTimeout time.Duration // execute sql timeout - TranTimeout time.Duration // transaction sql timeout - Breaker *breaker.Config // breaker -} - -// NewMySQL new db and retry connection when has error. -func NewMySQL(c *Config) (db *DB) { - if c.QueryTimeout == 0 || c.ExecTimeout == 0 || c.TranTimeout == 0 { - panic("mysql must be set query/execute/transction timeout") - } - db, err := Open(c) - if err != nil { - log.Error("open mysql error(%v)", err) - panic(err) - } - return -} diff --git a/pkg/database/sql/sql.go b/pkg/database/sql/sql.go deleted file mode 100644 index 4dbb2f683..000000000 --- a/pkg/database/sql/sql.go +++ /dev/null @@ -1,676 +0,0 @@ -package sql - -import ( - "context" - "database/sql" - "fmt" - "sync/atomic" - "time" - - "github.com/go-kratos/kratos/pkg/ecode" - "github.com/go-kratos/kratos/pkg/log" - "github.com/go-kratos/kratos/pkg/net/netutil/breaker" - "github.com/go-kratos/kratos/pkg/net/trace" - - "github.com/go-sql-driver/mysql" - "github.com/pkg/errors" -) - -const ( - _family = "sql_client" - _slowLogDuration = time.Millisecond * 250 -) - -var ( - // ErrStmtNil prepared stmt error - ErrStmtNil = errors.New("sql: prepare failed and stmt nil") - // ErrNoMaster is returned by Master when call master multiple times. - ErrNoMaster = errors.New("sql: no master instance") - // ErrNoRows is returned by Scan when QueryRow doesn't return a row. - // In such a case, QueryRow returns a placeholder *Row value that defers - // this error until a Scan. - ErrNoRows = sql.ErrNoRows - // ErrTxDone transaction done. - ErrTxDone = sql.ErrTxDone -) - -// DB database. -type DB struct { - write *conn - read []*conn - idx int64 - master *DB -} - -// conn database connection -type conn struct { - *sql.DB - breaker breaker.Breaker - conf *Config - addr string -} - -// Tx transaction. -type Tx struct { - db *conn - tx *sql.Tx - t trace.Trace - c context.Context - cancel func() -} - -// Row row. -type Row struct { - err error - *sql.Row - db *conn - query string - args []interface{} - t trace.Trace - cancel func() -} - -// Scan copies the columns from the matched row into the values pointed at by dest. -func (r *Row) Scan(dest ...interface{}) (err error) { - defer slowLog(fmt.Sprintf("Scan query(%s) args(%+v)", r.query, r.args), time.Now()) - if r.t != nil { - defer r.t.Finish(&err) - } - if r.err != nil { - err = r.err - } else if r.Row == nil { - err = ErrStmtNil - } - if err != nil { - return - } - err = r.Row.Scan(dest...) - if r.cancel != nil { - r.cancel() - } - r.db.onBreaker(&err) - if err != ErrNoRows { - err = errors.Wrapf(err, "query %s args %+v", r.query, r.args) - } - return -} - -// Rows rows. -type Rows struct { - *sql.Rows - cancel func() -} - -// Close closes the Rows, preventing further enumeration. If Next is called -// and returns false and there are no further result sets, -// the Rows are closed automatically and it will suffice to check the -// result of Err. Close is idempotent and does not affect the result of Err. -func (rs *Rows) Close() (err error) { - err = errors.WithStack(rs.Rows.Close()) - if rs.cancel != nil { - rs.cancel() - } - return -} - -// Stmt prepared stmt. -type Stmt struct { - db *conn - tx bool - query string - stmt atomic.Value - t trace.Trace -} - -// Open opens a database specified by its database driver name and a -// driver-specific data source name, usually consisting of at least a database -// name and connection information. -func Open(c *Config) (*DB, error) { - db := new(DB) - d, err := connect(c, c.DSN) - if err != nil { - return nil, err - } - addr := parseDSNAddr(c.DSN) - brkGroup := breaker.NewGroup(c.Breaker) - brk := brkGroup.Get(addr) - w := &conn{DB: d, breaker: brk, conf: c, addr: addr} - rs := make([]*conn, 0, len(c.ReadDSN)) - for _, rd := range c.ReadDSN { - d, err := connect(c, rd) - if err != nil { - return nil, err - } - addr = parseDSNAddr(rd) - brk := brkGroup.Get(addr) - r := &conn{DB: d, breaker: brk, conf: c, addr: addr} - rs = append(rs, r) - } - db.write = w - db.read = rs - db.master = &DB{write: db.write} - return db, nil -} - -func connect(c *Config, dataSourceName string) (*sql.DB, error) { - d, err := sql.Open("mysql", dataSourceName) - if err != nil { - err = errors.WithStack(err) - return nil, err - } - d.SetMaxOpenConns(c.Active) - d.SetMaxIdleConns(c.Idle) - d.SetConnMaxLifetime(time.Duration(c.IdleTimeout)) - return d, nil -} - -// Begin starts a transaction. The isolation level is dependent on the driver. -func (db *DB) Begin(c context.Context) (tx *Tx, err error) { - return db.write.begin(c) -} - -// Exec executes a query without returning any rows. -// The args are for any placeholder parameters in the query. -func (db *DB) Exec(c context.Context, query string, args ...interface{}) (res sql.Result, err error) { - return db.write.exec(c, query, args...) -} - -// Prepare creates a prepared statement for later queries or executions. -// Multiple queries or executions may be run concurrently from the returned -// statement. The caller must call the statement's Close method when the -// statement is no longer needed. -func (db *DB) Prepare(query string) (*Stmt, error) { - return db.write.prepare(query) -} - -// Prepared creates a prepared statement for later queries or executions. -// Multiple queries or executions may be run concurrently from the returned -// statement. The caller must call the statement's Close method when the -// statement is no longer needed. -func (db *DB) Prepared(query string) (stmt *Stmt) { - return db.write.prepared(query) -} - -// Query executes a query that returns rows, typically a SELECT. The args are -// for any placeholder parameters in the query. -func (db *DB) Query(c context.Context, query string, args ...interface{}) (rows *Rows, err error) { - idx := db.readIndex() - for i := range db.read { - if rows, err = db.read[(idx+i)%len(db.read)].query(c, query, args...); !ecode.EqualError(ecode.ServiceUnavailable, err) { - return - } - } - return db.write.query(c, query, args...) -} - -// QueryRow executes a query that is expected to return at most one row. -// QueryRow always returns a non-nil value. Errors are deferred until Row's -// Scan method is called. -func (db *DB) QueryRow(c context.Context, query string, args ...interface{}) *Row { - idx := db.readIndex() - for i := range db.read { - if row := db.read[(idx+i)%len(db.read)].queryRow(c, query, args...); !ecode.EqualError(ecode.ServiceUnavailable, row.err) { - return row - } - } - return db.write.queryRow(c, query, args...) -} - -func (db *DB) readIndex() int { - if len(db.read) == 0 { - return 0 - } - v := atomic.AddInt64(&db.idx, 1) - return int(v) % len(db.read) -} - -// Close closes the write and read database, releasing any open resources. -func (db *DB) Close() (err error) { - if e := db.write.Close(); e != nil { - err = errors.WithStack(e) - } - for _, rd := range db.read { - if e := rd.Close(); e != nil { - err = errors.WithStack(e) - } - } - return -} - -// Ping verifies a connection to the database is still alive, establishing a -// connection if necessary. -func (db *DB) Ping(c context.Context) (err error) { - if err = db.write.ping(c); err != nil { - return - } - for _, rd := range db.read { - if err = rd.ping(c); err != nil { - return - } - } - return -} - -// Master return *DB instance direct use master conn -// use this *DB instance only when you have some reason need to get result without any delay. -func (db *DB) Master() *DB { - if db.master == nil { - panic(ErrNoMaster) - } - return db.master -} - -func (db *conn) onBreaker(err *error) { - if err != nil && *err != nil && *err != sql.ErrNoRows && *err != sql.ErrTxDone { - db.breaker.MarkFailed() - } else { - db.breaker.MarkSuccess() - } -} - -func (db *conn) begin(c context.Context) (tx *Tx, err error) { - now := time.Now() - defer slowLog("Begin", now) - t, ok := trace.FromContext(c) - if ok { - t = t.Fork(_family, "begin") - t.SetTag(trace.String(trace.TagAddress, db.addr), trace.String(trace.TagComment, "")) - defer func() { - if err != nil { - t.Finish(&err) - } - }() - } - if err = db.breaker.Allow(); err != nil { - _metricReqErr.Inc(db.addr, db.addr, "begin", "breaker") - return - } - _, c, cancel := db.conf.TranTimeout.Shrink(c) - rtx, err := db.BeginTx(c, nil) - _metricReqDur.Observe(int64(time.Since(now)/time.Millisecond), db.addr, db.addr, "begin") - if err != nil { - err = errors.WithStack(err) - cancel() - return - } - tx = &Tx{tx: rtx, t: t, db: db, c: c, cancel: cancel} - return -} - -func (db *conn) exec(c context.Context, query string, args ...interface{}) (res sql.Result, err error) { - now := time.Now() - defer slowLog(fmt.Sprintf("Exec query(%s) args(%+v)", query, args), now) - if t, ok := trace.FromContext(c); ok { - t = t.Fork(_family, "exec") - t.SetTag(trace.String(trace.TagAddress, db.addr), trace.String(trace.TagComment, query)) - defer t.Finish(&err) - } - if err = db.breaker.Allow(); err != nil { - _metricReqErr.Inc(db.addr, db.addr, "exec", "breaker") - return - } - _, c, cancel := db.conf.ExecTimeout.Shrink(c) - res, err = db.ExecContext(c, query, args...) - cancel() - db.onBreaker(&err) - _metricReqDur.Observe(int64(time.Since(now)/time.Millisecond), db.addr, db.addr, "exec") - if err != nil { - err = errors.Wrapf(err, "exec:%s, args:%+v", query, args) - } - return -} - -func (db *conn) ping(c context.Context) (err error) { - now := time.Now() - defer slowLog("Ping", now) - if t, ok := trace.FromContext(c); ok { - t = t.Fork(_family, "ping") - t.SetTag(trace.String(trace.TagAddress, db.addr), trace.String(trace.TagComment, "")) - defer t.Finish(&err) - } - if err = db.breaker.Allow(); err != nil { - _metricReqErr.Inc(db.addr, db.addr, "ping", "breaker") - return - } - _, c, cancel := db.conf.ExecTimeout.Shrink(c) - err = db.PingContext(c) - cancel() - db.onBreaker(&err) - _metricReqDur.Observe(int64(time.Since(now)/time.Millisecond), db.addr, db.addr, "ping") - if err != nil { - err = errors.WithStack(err) - } - return -} - -func (db *conn) prepare(query string) (*Stmt, error) { - defer slowLog(fmt.Sprintf("Prepare query(%s)", query), time.Now()) - stmt, err := db.Prepare(query) - if err != nil { - err = errors.Wrapf(err, "prepare %s", query) - return nil, err - } - st := &Stmt{query: query, db: db} - st.stmt.Store(stmt) - return st, nil -} - -func (db *conn) prepared(query string) (stmt *Stmt) { - defer slowLog(fmt.Sprintf("Prepared query(%s)", query), time.Now()) - stmt = &Stmt{query: query, db: db} - s, err := db.Prepare(query) - if err == nil { - stmt.stmt.Store(s) - return - } - go func() { - for { - s, err := db.Prepare(query) - if err != nil { - time.Sleep(time.Second) - continue - } - stmt.stmt.Store(s) - return - } - }() - return -} - -func (db *conn) query(c context.Context, query string, args ...interface{}) (rows *Rows, err error) { - now := time.Now() - defer slowLog(fmt.Sprintf("Query query(%s) args(%+v)", query, args), now) - if t, ok := trace.FromContext(c); ok { - t = t.Fork(_family, "query") - t.SetTag(trace.String(trace.TagAddress, db.addr), trace.String(trace.TagComment, query)) - defer t.Finish(&err) - } - if err = db.breaker.Allow(); err != nil { - _metricReqErr.Inc(db.addr, db.addr, "query", "breaker") - return - } - _, c, cancel := db.conf.QueryTimeout.Shrink(c) - rs, err := db.DB.QueryContext(c, query, args...) - db.onBreaker(&err) - _metricReqDur.Observe(int64(time.Since(now)/time.Millisecond), db.addr, db.addr, "query") - if err != nil { - err = errors.Wrapf(err, "query:%s, args:%+v", query, args) - cancel() - return - } - rows = &Rows{Rows: rs, cancel: cancel} - return -} - -func (db *conn) queryRow(c context.Context, query string, args ...interface{}) *Row { - now := time.Now() - defer slowLog(fmt.Sprintf("QueryRow query(%s) args(%+v)", query, args), now) - t, ok := trace.FromContext(c) - if ok { - t = t.Fork(_family, "queryrow") - t.SetTag(trace.String(trace.TagAddress, db.addr), trace.String(trace.TagComment, query)) - } - if err := db.breaker.Allow(); err != nil { - _metricReqErr.Inc(db.addr, db.addr, "queryRow", "breaker") - return &Row{db: db, t: t, err: err} - } - _, c, cancel := db.conf.QueryTimeout.Shrink(c) - r := db.DB.QueryRowContext(c, query, args...) - _metricReqDur.Observe(int64(time.Since(now)/time.Millisecond), db.addr, db.addr, "queryrow") - return &Row{db: db, Row: r, query: query, args: args, t: t, cancel: cancel} -} - -// Close closes the statement. -func (s *Stmt) Close() (err error) { - if s == nil { - err = ErrStmtNil - return - } - stmt, ok := s.stmt.Load().(*sql.Stmt) - if ok { - err = errors.WithStack(stmt.Close()) - } - return -} - -// Exec executes a prepared statement with the given arguments and returns a -// Result summarizing the effect of the statement. -func (s *Stmt) Exec(c context.Context, args ...interface{}) (res sql.Result, err error) { - if s == nil { - err = ErrStmtNil - return - } - now := time.Now() - defer slowLog(fmt.Sprintf("Exec query(%s) args(%+v)", s.query, args), now) - if s.tx { - if s.t != nil { - s.t.SetTag(trace.String(trace.TagAnnotation, s.query)) - } - } else if t, ok := trace.FromContext(c); ok { - t = t.Fork(_family, "exec") - t.SetTag(trace.String(trace.TagAddress, s.db.addr), trace.String(trace.TagComment, s.query)) - defer t.Finish(&err) - } - if err = s.db.breaker.Allow(); err != nil { - _metricReqErr.Inc(s.db.addr, s.db.addr, "stmt:exec", "breaker") - return - } - stmt, ok := s.stmt.Load().(*sql.Stmt) - if !ok { - err = ErrStmtNil - return - } - _, c, cancel := s.db.conf.ExecTimeout.Shrink(c) - res, err = stmt.ExecContext(c, args...) - cancel() - s.db.onBreaker(&err) - _metricReqDur.Observe(int64(time.Since(now)/time.Millisecond), s.db.addr, s.db.addr, "stmt:exec") - if err != nil { - err = errors.Wrapf(err, "exec:%s, args:%+v", s.query, args) - } - return -} - -// Query executes a prepared query statement with the given arguments and -// returns the query results as a *Rows. -func (s *Stmt) Query(c context.Context, args ...interface{}) (rows *Rows, err error) { - if s == nil { - err = ErrStmtNil - return - } - now := time.Now() - defer slowLog(fmt.Sprintf("Query query(%s) args(%+v)", s.query, args), now) - if s.tx { - if s.t != nil { - s.t.SetTag(trace.String(trace.TagAnnotation, s.query)) - } - } else if t, ok := trace.FromContext(c); ok { - t = t.Fork(_family, "query") - t.SetTag(trace.String(trace.TagAddress, s.db.addr), trace.String(trace.TagComment, s.query)) - defer t.Finish(&err) - } - if err = s.db.breaker.Allow(); err != nil { - _metricReqErr.Inc(s.db.addr, s.db.addr, "stmt:query", "breaker") - return - } - stmt, ok := s.stmt.Load().(*sql.Stmt) - if !ok { - err = ErrStmtNil - return - } - _, c, cancel := s.db.conf.QueryTimeout.Shrink(c) - rs, err := stmt.QueryContext(c, args...) - s.db.onBreaker(&err) - _metricReqDur.Observe(int64(time.Since(now)/time.Millisecond), s.db.addr, s.db.addr, "stmt:query") - if err != nil { - err = errors.Wrapf(err, "query:%s, args:%+v", s.query, args) - cancel() - return - } - rows = &Rows{Rows: rs, cancel: cancel} - return -} - -// QueryRow executes a prepared query statement with the given arguments. -// If an error occurs during the execution of the statement, that error will -// be returned by a call to Scan on the returned *Row, which is always non-nil. -// If the query selects no rows, the *Row's Scan will return ErrNoRows. -// Otherwise, the *Row's Scan scans the first selected row and discards the rest. -func (s *Stmt) QueryRow(c context.Context, args ...interface{}) (row *Row) { - now := time.Now() - defer slowLog(fmt.Sprintf("QueryRow query(%s) args(%+v)", s.query, args), now) - row = &Row{db: s.db, query: s.query, args: args} - if s == nil { - row.err = ErrStmtNil - return - } - if s.tx { - if s.t != nil { - s.t.SetTag(trace.String(trace.TagAnnotation, s.query)) - } - } else if t, ok := trace.FromContext(c); ok { - t = t.Fork(_family, "queryrow") - t.SetTag(trace.String(trace.TagAddress, s.db.addr), trace.String(trace.TagComment, s.query)) - row.t = t - } - if row.err = s.db.breaker.Allow(); row.err != nil { - _metricReqErr.Inc(s.db.addr, s.db.addr, "stmt:queryrow", "breaker") - return - } - stmt, ok := s.stmt.Load().(*sql.Stmt) - if !ok { - return - } - _, c, cancel := s.db.conf.QueryTimeout.Shrink(c) - row.Row = stmt.QueryRowContext(c, args...) - row.cancel = cancel - _metricReqDur.Observe(int64(time.Since(now)/time.Millisecond), s.db.addr, s.db.addr, "stmt:queryrow") - return -} - -// Commit commits the transaction. -func (tx *Tx) Commit() (err error) { - err = tx.tx.Commit() - tx.cancel() - tx.db.onBreaker(&err) - if tx.t != nil { - tx.t.Finish(&err) - } - if err != nil { - err = errors.WithStack(err) - } - return -} - -// Rollback aborts the transaction. -func (tx *Tx) Rollback() (err error) { - err = tx.tx.Rollback() - tx.cancel() - tx.db.onBreaker(&err) - if tx.t != nil { - tx.t.Finish(&err) - } - if err != nil { - err = errors.WithStack(err) - } - return -} - -// Exec executes a query that doesn't return rows. For example: an INSERT and -// UPDATE. -func (tx *Tx) Exec(query string, args ...interface{}) (res sql.Result, err error) { - now := time.Now() - defer slowLog(fmt.Sprintf("Exec query(%s) args(%+v)", query, args), now) - if tx.t != nil { - tx.t.SetTag(trace.String(trace.TagAnnotation, fmt.Sprintf("exec %s", query))) - } - res, err = tx.tx.ExecContext(tx.c, query, args...) - _metricReqDur.Observe(int64(time.Since(now)/time.Millisecond), tx.db.addr, tx.db.addr, "tx:exec") - if err != nil { - err = errors.Wrapf(err, "exec:%s, args:%+v", query, args) - } - return -} - -// Query executes a query that returns rows, typically a SELECT. -func (tx *Tx) Query(query string, args ...interface{}) (rows *Rows, err error) { - if tx.t != nil { - tx.t.SetTag(trace.String(trace.TagAnnotation, fmt.Sprintf("query %s", query))) - } - now := time.Now() - defer slowLog(fmt.Sprintf("Query query(%s) args(%+v)", query, args), now) - defer func() { - _metricReqDur.Observe(int64(time.Since(now)/time.Millisecond), tx.db.addr, tx.db.addr, "tx:query") - }() - rs, err := tx.tx.QueryContext(tx.c, query, args...) - if err == nil { - rows = &Rows{Rows: rs} - } else { - err = errors.Wrapf(err, "query:%s, args:%+v", query, args) - } - return -} - -// QueryRow executes a query that is expected to return at most one row. -// QueryRow always returns a non-nil value. Errors are deferred until Row's -// Scan method is called. -func (tx *Tx) QueryRow(query string, args ...interface{}) *Row { - if tx.t != nil { - tx.t.SetTag(trace.String(trace.TagAnnotation, fmt.Sprintf("queryrow %s", query))) - } - now := time.Now() - defer slowLog(fmt.Sprintf("QueryRow query(%s) args(%+v)", query, args), now) - defer func() { - _metricReqDur.Observe(int64(time.Since(now)/time.Millisecond), tx.db.addr, tx.db.addr, "tx:queryrow") - }() - r := tx.tx.QueryRowContext(tx.c, query, args...) - return &Row{Row: r, db: tx.db, query: query, args: args} -} - -// Stmt returns a transaction-specific prepared statement from an existing statement. -func (tx *Tx) Stmt(stmt *Stmt) *Stmt { - as, ok := stmt.stmt.Load().(*sql.Stmt) - if !ok { - return nil - } - ts := tx.tx.StmtContext(tx.c, as) - st := &Stmt{query: stmt.query, tx: true, t: tx.t, db: tx.db} - st.stmt.Store(ts) - return st -} - -// Prepare creates a prepared statement for use within a transaction. -// The returned statement operates within the transaction and can no longer be -// used once the transaction has been committed or rolled back. -// To use an existing prepared statement on this transaction, see Tx.Stmt. -func (tx *Tx) Prepare(query string) (*Stmt, error) { - if tx.t != nil { - tx.t.SetTag(trace.String(trace.TagAnnotation, fmt.Sprintf("prepare %s", query))) - } - defer slowLog(fmt.Sprintf("Prepare query(%s)", query), time.Now()) - stmt, err := tx.tx.Prepare(query) - if err != nil { - err = errors.Wrapf(err, "prepare %s", query) - return nil, err - } - st := &Stmt{query: query, tx: true, t: tx.t, db: tx.db} - st.stmt.Store(stmt) - return st, nil -} - -// parseDSNAddr parse dsn name and return addr. -func parseDSNAddr(dsn string) (addr string) { - cfg, err := mysql.ParseDSN(dsn) - if err != nil { - // just ignore parseDSN error, mysql client will return error for us when connect. - return "" - } - return cfg.Addr -} - -func slowLog(statement string, now time.Time) { - du := time.Since(now) - if du > _slowLogDuration { - log.Warn("%s slow log statement: %s time: %v", _family, statement, du) - } -} diff --git a/pkg/database/sql/sql_test.go b/pkg/database/sql/sql_test.go deleted file mode 100644 index 191ab7937..000000000 --- a/pkg/database/sql/sql_test.go +++ /dev/null @@ -1,18 +0,0 @@ -package sql - -import ( - "testing" - - "github.com/stretchr/testify/assert" -) - -func TestParseAddrDSN(t *testing.T) { - t.Run("test parse addr dsn", func(t *testing.T) { - addr := parseDSNAddr("test:test@tcp(172.16.0.148:3306)/test?timeout=5s&readTimeout=5s&writeTimeout=5s&parseTime=true&loc=Local&charset=utf8") - assert.Equal(t, "172.16.0.148:3306", addr) - }) - t.Run("test password has @", func(t *testing.T) { - addr := parseDSNAddr("root:root@dev@tcp(1.2.3.4:3306)/abc?timeout=1s&readTimeout=1s&writeTimeout=1s&parseTime=true&loc=Local&charset=utf8mb4,utf8") - assert.Equal(t, "1.2.3.4:3306", addr) - }) -} diff --git a/pkg/database/tidb/README.md b/pkg/database/tidb/README.md deleted file mode 100644 index 91b525782..000000000 --- a/pkg/database/tidb/README.md +++ /dev/null @@ -1,14 +0,0 @@ -#### database/tidb - -##### 项目简介 -TiDB数据库驱动 对mysql驱动进行封装 - -##### 功能 -1. 支持discovery服务发现 多节点直连 -2. 支持通过lvs单一地址连接 -3. 支持prepare绑定多个节点 -4. 支持动态增减节点负载均衡 -5. 日志区分运行节点 - -##### 依赖包 -1.[Go-MySQL-Driver](https://github.com/go-sql-driver/mysql) diff --git a/pkg/database/tidb/discovery.go b/pkg/database/tidb/discovery.go deleted file mode 100644 index fb728c2aa..000000000 --- a/pkg/database/tidb/discovery.go +++ /dev/null @@ -1,56 +0,0 @@ -package tidb - -import ( - "context" - "fmt" - "strings" - "time" - - "github.com/go-kratos/kratos/pkg/conf/env" - "github.com/go-kratos/kratos/pkg/log" - "github.com/go-kratos/kratos/pkg/naming" - "github.com/go-kratos/kratos/pkg/naming/discovery" -) - -var _schema = "tidb://" - -func (db *DB) nodeList() (nodes []string) { - var ( - insZone *naming.InstancesInfo - ins []*naming.Instance - ok bool - ) - if insZone, ok = db.dis.Fetch(context.Background()); !ok { - return - } - if ins, ok = insZone.Instances[env.Zone]; !ok || len(ins) == 0 { - return - } - for _, in := range ins { - for _, addr := range in.Addrs { - if strings.HasPrefix(addr, _schema) { - addr = strings.Replace(addr, _schema, "", -1) - nodes = append(nodes, addr) - } - } - } - log.Info("tidb get %s instances(%v)", db.appid, nodes) - return -} - -func (db *DB) disc() (nodes []string) { - db.dis = discovery.Build(db.appid) - e := db.dis.Watch() - select { - case <-e: - nodes = db.nodeList() - case <-time.After(10 * time.Second): - panic("tidb init discovery err") - } - if len(nodes) == 0 { - panic(fmt.Sprintf("tidb %s no instance", db.appid)) - } - go db.nodeproc(e) - log.Info("init tidb discvoery info successfully") - return -} diff --git a/pkg/database/tidb/metrics.go b/pkg/database/tidb/metrics.go deleted file mode 100644 index 96687ed16..000000000 --- a/pkg/database/tidb/metrics.go +++ /dev/null @@ -1,37 +0,0 @@ -package tidb - -import "github.com/go-kratos/kratos/pkg/stat/metric" - -const namespace = "tidb_client" - -var ( - _metricReqDur = metric.NewHistogramVec(&metric.HistogramVecOpts{ - Namespace: namespace, - Subsystem: "requests", - Name: "duration_ms", - Help: "tidb client requests duration(ms).", - Labels: []string{"name", "addr", "command"}, - Buckets: []float64{5, 10, 25, 50, 100, 250, 500, 1000, 2500}, - }) - _metricReqErr = metric.NewCounterVec(&metric.CounterVecOpts{ - Namespace: namespace, - Subsystem: "requests", - Name: "error_total", - Help: "tidb client requests error count.", - Labels: []string{"name", "addr", "command", "error"}, - }) - _metricConnTotal = metric.NewCounterVec(&metric.CounterVecOpts{ - Namespace: namespace, - Subsystem: "connections", - Name: "total", - Help: "tidb client connections total count.", - Labels: []string{"name", "addr", "state"}, - }) - _metricConnCurrent = metric.NewGaugeVec(&metric.GaugeVecOpts{ - Namespace: namespace, - Subsystem: "connections", - Name: "current", - Help: "tidb client connections current.", - Labels: []string{"name", "addr", "state"}, - }) -) diff --git a/pkg/database/tidb/node_proc.go b/pkg/database/tidb/node_proc.go deleted file mode 100644 index 0b72acdc5..000000000 --- a/pkg/database/tidb/node_proc.go +++ /dev/null @@ -1,82 +0,0 @@ -package tidb - -import ( - "time" - - "github.com/go-kratos/kratos/pkg/log" -) - -func (db *DB) nodeproc(e <-chan struct{}) { - if db.dis == nil { - return - } - for { - <-e - nodes := db.nodeList() - if len(nodes) == 0 { - continue - } - cm := make(map[string]*conn) - var conns []*conn - for _, conn := range db.conns { - cm[conn.addr] = conn - } - for _, node := range nodes { - if cm[node] != nil { - conns = append(conns, cm[node]) - continue - } - c, err := db.connectDSN(genDSN(db.conf.DSN, node)) - if err == nil { - conns = append(conns, c) - } else { - log.Error("tidb: connect addr: %s err: %+v", node, err) - } - } - if len(conns) == 0 { - log.Error("tidb: no nodes ignore event") - continue - } - oldConns := db.conns - db.mutex.Lock() - db.conns = conns - db.mutex.Unlock() - log.Info("tidb: new nodes: %v", nodes) - var removedConn []*conn - for _, conn := range oldConns { - var exist bool - for _, c := range conns { - if c.addr == conn.addr { - exist = true - break - } - } - if !exist { - removedConn = append(removedConn, conn) - } - } - go db.closeConns(removedConn) - } -} - -func (db *DB) closeConns(conns []*conn) { - if len(conns) == 0 { - return - } - du := db.conf.QueryTimeout - if db.conf.ExecTimeout > du { - du = db.conf.ExecTimeout - } - if db.conf.TranTimeout > du { - du = db.conf.TranTimeout - } - time.Sleep(time.Duration(du)) - for _, conn := range conns { - err := conn.Close() - if err != nil { - log.Error("tidb: close removed conn: %s err: %v", conn.addr, err) - } else { - log.Info("tidb: close removed conn: %s", conn.addr) - } - } -} diff --git a/pkg/database/tidb/sql.go b/pkg/database/tidb/sql.go deleted file mode 100644 index ee932940e..000000000 --- a/pkg/database/tidb/sql.go +++ /dev/null @@ -1,739 +0,0 @@ -package tidb - -import ( - "context" - "database/sql" - "fmt" - "sync" - "sync/atomic" - "time" - - "github.com/go-kratos/kratos/pkg/log" - "github.com/go-kratos/kratos/pkg/naming" - "github.com/go-kratos/kratos/pkg/net/netutil/breaker" - "github.com/go-kratos/kratos/pkg/net/trace" - - "github.com/go-sql-driver/mysql" - "github.com/pkg/errors" -) - -const ( - _family = "tidb_client" - _slowLogDuration = time.Millisecond * 250 -) - -var ( - // ErrStmtNil prepared stmt error - ErrStmtNil = errors.New("sql: prepare failed and stmt nil") - // ErrNoRows is returned by Scan when QueryRow doesn't return a row. - // In such a case, QueryRow returns a placeholder *Row value that defers - // this error until a Scan. - ErrNoRows = sql.ErrNoRows - // ErrTxDone transaction done. - ErrTxDone = sql.ErrTxDone -) - -// DB database. -type DB struct { - conf *Config - conns []*conn - idx int64 - dis naming.Resolver - appid string - mutex sync.RWMutex - breakerGroup *breaker.Group -} - -// conn database connection -type conn struct { - *sql.DB - breaker breaker.Breaker - conf *Config - addr string -} - -// Tx transaction. -type Tx struct { - db *conn - tx *sql.Tx - t trace.Trace - c context.Context - cancel func() -} - -// Row row. -type Row struct { - err error - *sql.Row - db *conn - query string - args []interface{} - t trace.Trace - cancel func() -} - -// Scan copies the columns from the matched row into the values pointed at by dest. -func (r *Row) Scan(dest ...interface{}) (err error) { - defer slowLog(fmt.Sprintf("Scan addr: %s query(%s) args(%+v)", r.db.addr, r.query, r.args), time.Now()) - if r.t != nil { - defer r.t.Finish(&err) - } - if r.err != nil { - err = r.err - } else if r.Row == nil { - err = ErrStmtNil - } - if err != nil { - return - } - err = r.Row.Scan(dest...) - if r.cancel != nil { - r.cancel() - } - r.db.onBreaker(&err) - if err != ErrNoRows { - err = errors.Wrapf(err, "addr: %s, query %s args %+v", r.db.addr, r.query, r.args) - } - return -} - -// Rows rows. -type Rows struct { - *sql.Rows - cancel func() -} - -// Close closes the Rows, preventing further enumeration. If Next is called -// and returns false and there are no further result sets, -// the Rows are closed automatically and it will suffice to check the -// result of Err. Close is idempotent and does not affect the result of Err. -func (rs *Rows) Close() (err error) { - err = errors.WithStack(rs.Rows.Close()) - if rs.cancel != nil { - rs.cancel() - } - return -} - -// Stmt prepared stmt. -type Stmt struct { - db *conn - tx bool - query string - stmt atomic.Value - t trace.Trace -} - -// Stmts random prepared stmt. -type Stmts struct { - query string - sts map[string]*Stmt - mu sync.RWMutex - db *DB -} - -// Open opens a database specified by its database driver name and a -// driver-specific data source name, usually consisting of at least a database -// name and connection information. -func Open(c *Config) (db *DB, err error) { - db = &DB{conf: c, breakerGroup: breaker.NewGroup(c.Breaker)} - cfg, err := mysql.ParseDSN(c.DSN) - if err != nil { - return - } - var dsns []string - if cfg.Net == "discovery" { - db.appid = cfg.Addr - for _, addr := range db.disc() { - dsns = append(dsns, genDSN(c.DSN, addr)) - } - } else { - dsns = append(dsns, c.DSN) - } - - cs := make([]*conn, 0, len(dsns)) - for _, dsn := range dsns { - r, err := db.connectDSN(dsn) - if err != nil { - return db, err - } - cs = append(cs, r) - } - db.conns = cs - return -} - -func (db *DB) connectDSN(dsn string) (c *conn, err error) { - d, err := connect(db.conf, dsn) - if err != nil { - return - } - addr := parseDSNAddr(dsn) - brk := db.breakerGroup.Get(addr) - c = &conn{DB: d, breaker: brk, conf: db.conf, addr: addr} - return -} - -func connect(c *Config, dataSourceName string) (*sql.DB, error) { - d, err := sql.Open("mysql", dataSourceName) - if err != nil { - err = errors.WithStack(err) - return nil, err - } - d.SetMaxOpenConns(c.Active) - d.SetMaxIdleConns(c.Idle) - d.SetConnMaxLifetime(time.Duration(c.IdleTimeout)) - return d, nil -} - -func (db *DB) conn() (c *conn) { - db.mutex.RLock() - c = db.conns[db.index()] - db.mutex.RUnlock() - return -} - -// Begin starts a transaction. The isolation level is dependent on the driver. -func (db *DB) Begin(c context.Context) (tx *Tx, err error) { - return db.conn().begin(c) -} - -// Exec executes a query without returning any rows. -// The args are for any placeholder parameters in the query. -func (db *DB) Exec(c context.Context, query string, args ...interface{}) (res sql.Result, err error) { - return db.conn().exec(c, query, args...) -} - -// Prepare creates a prepared statement for later queries or executions. -// Multiple queries or executions may be run concurrently from the returned -// statement. The caller must call the statement's Close method when the -// statement is no longer needed. -func (db *DB) Prepare(query string) (*Stmt, error) { - return db.conn().prepare(query) -} - -// Prepared creates a prepared statement for later queries or executions. -// Multiple queries or executions may be run concurrently from the returned -// statement. The caller must call the statement's Close method when the -// statement is no longer needed. -func (db *DB) Prepared(query string) (s *Stmts) { - s = &Stmts{query: query, sts: make(map[string]*Stmt), db: db} - for _, c := range db.conns { - st := c.prepared(query) - s.mu.Lock() - s.sts[c.addr] = st - s.mu.Unlock() - } - return -} - -// Query executes a query that returns rows, typically a SELECT. The args are -// for any placeholder parameters in the query. -func (db *DB) Query(c context.Context, query string, args ...interface{}) (rows *Rows, err error) { - return db.conn().query(c, query, args...) -} - -// QueryRow executes a query that is expected to return at most one row. -// QueryRow always returns a non-nil value. Errors are deferred until Row's -// Scan method is called. -func (db *DB) QueryRow(c context.Context, query string, args ...interface{}) *Row { - return db.conn().queryRow(c, query, args...) -} - -func (db *DB) index() int { - if len(db.conns) == 1 { - return 0 - } - v := atomic.AddInt64(&db.idx, 1) - return int(v) % len(db.conns) -} - -// Close closes the databases, releasing any open resources. -func (db *DB) Close() (err error) { - db.mutex.RLock() - defer db.mutex.RUnlock() - for _, d := range db.conns { - if e := d.Close(); e != nil { - err = errors.WithStack(e) - } - } - return -} - -// Ping verifies a connection to the database is still alive, establishing a -// connection if necessary. -func (db *DB) Ping(c context.Context) (err error) { - if err = db.conn().ping(c); err != nil { - return - } - return -} - -func (db *conn) onBreaker(err *error) { - if err != nil && *err != nil && *err != sql.ErrNoRows && *err != sql.ErrTxDone { - db.breaker.MarkFailed() - } else { - db.breaker.MarkSuccess() - } -} - -func (db *conn) begin(c context.Context) (tx *Tx, err error) { - now := time.Now() - defer slowLog(fmt.Sprintf("Begin addr: %s", db.addr), now) - t, ok := trace.FromContext(c) - if ok { - t = t.Fork(_family, "begin") - t.SetTag(trace.String(trace.TagAddress, db.addr), trace.String(trace.TagComment, "")) - defer func() { - if err != nil { - t.Finish(&err) - } - }() - } - if err = db.breaker.Allow(); err != nil { - _metricReqErr.Inc(db.addr, db.addr, "begin", "breaker") - return - } - _, c, cancel := db.conf.TranTimeout.Shrink(c) - rtx, err := db.BeginTx(c, nil) - _metricReqDur.Observe(int64(time.Since(now)/time.Millisecond), db.addr, db.addr, "begin") - if err != nil { - err = errors.WithStack(err) - cancel() - return - } - tx = &Tx{tx: rtx, t: t, db: db, c: c, cancel: cancel} - return -} - -func (db *conn) exec(c context.Context, query string, args ...interface{}) (res sql.Result, err error) { - now := time.Now() - defer slowLog(fmt.Sprintf("Exec addr: %s query(%s) args(%+v)", db.addr, query, args), now) - if t, ok := trace.FromContext(c); ok { - t = t.Fork(_family, "exec") - t.SetTag(trace.String(trace.TagAddress, db.addr), trace.String(trace.TagComment, query)) - defer t.Finish(&err) - } - if err = db.breaker.Allow(); err != nil { - _metricReqErr.Inc(db.addr, db.addr, "exec", "breaker") - return - } - _, c, cancel := db.conf.ExecTimeout.Shrink(c) - res, err = db.ExecContext(c, query, args...) - cancel() - db.onBreaker(&err) - _metricReqDur.Observe(int64(time.Since(now)/time.Millisecond), db.addr, db.addr, "exec") - if err != nil { - err = errors.Wrapf(err, "addr: %s exec:%s, args:%+v", db.addr, query, args) - } - return -} - -func (db *conn) ping(c context.Context) (err error) { - now := time.Now() - defer slowLog(fmt.Sprintf("Ping addr: %s", db.addr), now) - if t, ok := trace.FromContext(c); ok { - t = t.Fork(_family, "ping") - t.SetTag(trace.String(trace.TagAddress, db.addr), trace.String(trace.TagComment, "")) - defer t.Finish(&err) - } - if err = db.breaker.Allow(); err != nil { - _metricReqErr.Inc(db.addr, db.addr, "ping", "breaker") - return - } - _, c, cancel := db.conf.ExecTimeout.Shrink(c) - err = db.PingContext(c) - cancel() - db.onBreaker(&err) - _metricReqDur.Observe(int64(time.Since(now)/time.Millisecond), db.addr, db.addr, "ping") - if err != nil { - err = errors.WithStack(err) - } - return -} - -func (db *conn) prepare(query string) (*Stmt, error) { - defer slowLog(fmt.Sprintf("Prepare addr: %s query(%s)", db.addr, query), time.Now()) - stmt, err := db.Prepare(query) - if err != nil { - err = errors.Wrapf(err, "addr: %s prepare %s", db.addr, query) - return nil, err - } - st := &Stmt{query: query, db: db} - st.stmt.Store(stmt) - return st, nil -} - -func (db *conn) prepared(query string) (stmt *Stmt) { - defer slowLog(fmt.Sprintf("Prepared addr: %s query(%s)", db.addr, query), time.Now()) - stmt = &Stmt{query: query, db: db} - s, err := db.Prepare(query) - if err == nil { - stmt.stmt.Store(s) - return - } - return -} - -func (db *conn) query(c context.Context, query string, args ...interface{}) (rows *Rows, err error) { - now := time.Now() - defer slowLog(fmt.Sprintf("Query addr: %s query(%s) args(%+v)", db.addr, query, args), now) - if t, ok := trace.FromContext(c); ok { - t = t.Fork(_family, "query") - t.SetTag(trace.String(trace.TagAddress, db.addr), trace.String(trace.TagComment, query)) - defer t.Finish(&err) - } - if err = db.breaker.Allow(); err != nil { - _metricReqErr.Inc(db.addr, db.addr, "query", "breaker") - return - } - _, c, cancel := db.conf.QueryTimeout.Shrink(c) - rs, err := db.DB.QueryContext(c, query, args...) - db.onBreaker(&err) - _metricReqDur.Observe(int64(time.Since(now)/time.Millisecond), db.addr, db.addr, "query") - if err != nil { - err = errors.Wrapf(err, "addr: %s, query:%s, args:%+v", db.addr, query, args) - cancel() - return - } - rows = &Rows{Rows: rs, cancel: cancel} - return -} - -func (db *conn) queryRow(c context.Context, query string, args ...interface{}) *Row { - now := time.Now() - defer slowLog(fmt.Sprintf("QueryRow addr: %s query(%s) args(%+v)", db.addr, query, args), now) - t, ok := trace.FromContext(c) - if ok { - t = t.Fork(_family, "queryrow") - t.SetTag(trace.String(trace.TagAddress, db.addr), trace.String(trace.TagComment, query)) - } - if err := db.breaker.Allow(); err != nil { - _metricReqErr.Inc(db.addr, db.addr, "queryrow", "breaker") - return &Row{db: db, t: t, err: err} - } - _, c, cancel := db.conf.QueryTimeout.Shrink(c) - r := db.DB.QueryRowContext(c, query, args...) - _metricReqDur.Observe(int64(time.Since(now)/time.Millisecond), db.addr, db.addr, "queryrow") - return &Row{db: db, Row: r, query: query, args: args, t: t, cancel: cancel} -} - -// Close closes the statement. -func (s *Stmt) Close() (err error) { - stmt, ok := s.stmt.Load().(*sql.Stmt) - if ok { - err = errors.WithStack(stmt.Close()) - } - return -} - -func (s *Stmt) prepare() (st *sql.Stmt) { - var ok bool - if st, ok = s.stmt.Load().(*sql.Stmt); ok { - return - } - var err error - if st, err = s.db.Prepare(s.query); err == nil { - s.stmt.Store(st) - } - return -} - -// Exec executes a prepared statement with the given arguments and returns a -// Result summarizing the effect of the statement. -func (s *Stmt) Exec(c context.Context, args ...interface{}) (res sql.Result, err error) { - now := time.Now() - defer slowLog(fmt.Sprintf("Exec addr: %s query(%s) args(%+v)", s.db.addr, s.query, args), now) - if s.tx { - if s.t != nil { - s.t.SetTag(trace.String(trace.TagAnnotation, s.query)) - } - } else if t, ok := trace.FromContext(c); ok { - t = t.Fork(_family, "exec") - t.SetTag(trace.String(trace.TagAddress, s.db.addr), trace.String(trace.TagComment, s.query)) - defer t.Finish(&err) - } - if err = s.db.breaker.Allow(); err != nil { - _metricReqErr.Inc(s.db.addr, s.db.addr, "stmt:exec", "breaker") - return - } - stmt := s.prepare() - if stmt == nil { - err = ErrStmtNil - return - } - _, c, cancel := s.db.conf.ExecTimeout.Shrink(c) - res, err = stmt.ExecContext(c, args...) - cancel() - s.db.onBreaker(&err) - _metricReqDur.Observe(int64(time.Since(now)/time.Millisecond), s.db.addr, s.db.addr, "stmt:exec") - if err != nil { - err = errors.Wrapf(err, "addr: %s exec:%s, args:%+v", s.db.addr, s.query, args) - } - return -} - -// Query executes a prepared query statement with the given arguments and -// returns the query results as a *Rows. -func (s *Stmt) Query(c context.Context, args ...interface{}) (rows *Rows, err error) { - now := time.Now() - defer slowLog(fmt.Sprintf("Query addr: %s query(%s) args(%+v)", s.db.addr, s.query, args), now) - if s.tx { - if s.t != nil { - s.t.SetTag(trace.String(trace.TagAnnotation, s.query)) - } - } else if t, ok := trace.FromContext(c); ok { - t = t.Fork(_family, "query") - t.SetTag(trace.String(trace.TagAddress, s.db.addr), trace.String(trace.TagComment, s.query)) - defer t.Finish(&err) - } - if err = s.db.breaker.Allow(); err != nil { - _metricReqErr.Inc(s.db.addr, s.db.addr, "stmt:query", "breaker") - return - } - stmt := s.prepare() - if stmt == nil { - err = ErrStmtNil - return - } - _, c, cancel := s.db.conf.QueryTimeout.Shrink(c) - rs, err := stmt.QueryContext(c, args...) - s.db.onBreaker(&err) - _metricReqDur.Observe(int64(time.Since(now)/time.Millisecond), s.db.addr, s.db.addr, "stmt:query") - if err != nil { - err = errors.Wrapf(err, "addr: %s, query:%s, args:%+v", s.db.addr, s.query, args) - cancel() - return - } - rows = &Rows{Rows: rs, cancel: cancel} - return -} - -// QueryRow executes a prepared query statement with the given arguments. -// If an error occurs during the execution of the statement, that error will -// be returned by a call to Scan on the returned *Row, which is always non-nil. -// If the query selects no rows, the *Row's Scan will return ErrNoRows. -// Otherwise, the *Row's Scan scans the first selected row and discards the rest. -func (s *Stmt) QueryRow(c context.Context, args ...interface{}) (row *Row) { - now := time.Now() - defer slowLog(fmt.Sprintf("QueryRow addr: %s query(%s) args(%+v)", s.db.addr, s.query, args), now) - row = &Row{db: s.db, query: s.query, args: args} - if s.tx { - if s.t != nil { - s.t.SetTag(trace.String(trace.TagAnnotation, s.query)) - } - } else if t, ok := trace.FromContext(c); ok { - t = t.Fork(_family, "queryrow") - t.SetTag(trace.String(trace.TagAddress, s.db.addr), trace.String(trace.TagComment, s.query)) - row.t = t - } - if row.err = s.db.breaker.Allow(); row.err != nil { - _metricReqErr.Inc(s.db.addr, s.db.addr, "stmt:queryrow", "breaker") - return - } - stmt := s.prepare() - if stmt == nil { - return - } - _, c, cancel := s.db.conf.QueryTimeout.Shrink(c) - row.Row = stmt.QueryRowContext(c, args...) - row.cancel = cancel - _metricReqDur.Observe(int64(time.Since(now)/time.Millisecond), s.db.addr, s.db.addr, "stmt:queryrow") - return -} - -func (s *Stmts) prepare(conn *conn) (st *Stmt) { - if conn == nil { - conn = s.db.conn() - } - s.mu.RLock() - st = s.sts[conn.addr] - s.mu.RUnlock() - if st == nil { - st = conn.prepared(s.query) - s.mu.Lock() - s.sts[conn.addr] = st - s.mu.Unlock() - } - return -} - -// Exec executes a prepared statement with the given arguments and returns a -// Result summarizing the effect of the statement. -func (s *Stmts) Exec(c context.Context, args ...interface{}) (res sql.Result, err error) { - return s.prepare(nil).Exec(c, args...) -} - -// Query executes a prepared query statement with the given arguments and -// returns the query results as a *Rows. -func (s *Stmts) Query(c context.Context, args ...interface{}) (rows *Rows, err error) { - return s.prepare(nil).Query(c, args...) -} - -// QueryRow executes a prepared query statement with the given arguments. -// If an error occurs during the execution of the statement, that error will -// be returned by a call to Scan on the returned *Row, which is always non-nil. -// If the query selects no rows, the *Row's Scan will return ErrNoRows. -// Otherwise, the *Row's Scan scans the first selected row and discards the rest. -func (s *Stmts) QueryRow(c context.Context, args ...interface{}) (row *Row) { - return s.prepare(nil).QueryRow(c, args...) -} - -// Close closes the statement. -func (s *Stmts) Close() (err error) { - for _, st := range s.sts { - if err = errors.WithStack(st.Close()); err != nil { - return - } - } - return -} - -// Commit commits the transaction. -func (tx *Tx) Commit() (err error) { - err = tx.tx.Commit() - tx.cancel() - tx.db.onBreaker(&err) - if tx.t != nil { - tx.t.Finish(&err) - } - if err != nil { - err = errors.WithStack(err) - } - return -} - -// Rollback aborts the transaction. -func (tx *Tx) Rollback() (err error) { - err = tx.tx.Rollback() - tx.cancel() - tx.db.onBreaker(&err) - if tx.t != nil { - tx.t.Finish(&err) - } - if err != nil { - err = errors.WithStack(err) - } - return -} - -// Exec executes a query that doesn't return rows. For example: an INSERT and -// UPDATE. -func (tx *Tx) Exec(query string, args ...interface{}) (res sql.Result, err error) { - now := time.Now() - defer slowLog(fmt.Sprintf("Exec addr: %s query(%s) args(%+v)", tx.db.addr, query, args), now) - if tx.t != nil { - tx.t.SetTag(trace.String(trace.TagAnnotation, fmt.Sprintf("exec %s", query))) - } - res, err = tx.tx.ExecContext(tx.c, query, args...) - _metricReqDur.Observe(int64(time.Since(now)/time.Millisecond), tx.db.addr, tx.db.addr, "tx:exec") - if err != nil { - err = errors.Wrapf(err, "addr: %s exec:%s, args:%+v", tx.db.addr, query, args) - } - return -} - -// Query executes a query that returns rows, typically a SELECT. -func (tx *Tx) Query(query string, args ...interface{}) (rows *Rows, err error) { - if tx.t != nil { - tx.t.SetTag(trace.String(trace.TagAnnotation, fmt.Sprintf("query %s", query))) - } - now := time.Now() - defer slowLog(fmt.Sprintf("Query addr: %s query(%s) args(%+v)", tx.db.addr, query, args), now) - defer func() { - _metricReqDur.Observe(int64(time.Since(now)/time.Millisecond), tx.db.addr, tx.db.addr, "tx:query") - }() - rs, err := tx.tx.QueryContext(tx.c, query, args...) - if err == nil { - rows = &Rows{Rows: rs} - } else { - err = errors.Wrapf(err, "addr: %s, query:%s, args:%+v", tx.db.addr, query, args) - } - return -} - -// QueryRow executes a query that is expected to return at most one row. -// QueryRow always returns a non-nil value. Errors are deferred until Row's -// Scan method is called. -func (tx *Tx) QueryRow(query string, args ...interface{}) *Row { - if tx.t != nil { - tx.t.SetTag(trace.String(trace.TagAnnotation, fmt.Sprintf("queryrow %s", query))) - } - now := time.Now() - defer slowLog(fmt.Sprintf("QueryRow addr: %s query(%s) args(%+v)", tx.db.addr, query, args), now) - defer func() { - _metricReqDur.Observe(int64(time.Since(now)/time.Millisecond), tx.db.addr, tx.db.addr, "tx:queryrow") - }() - r := tx.tx.QueryRowContext(tx.c, query, args...) - return &Row{Row: r, db: tx.db, query: query, args: args} -} - -// Stmt returns a transaction-specific prepared statement from an existing statement. -func (tx *Tx) Stmt(stmt *Stmt) *Stmt { - if stmt == nil { - return nil - } - as, ok := stmt.stmt.Load().(*sql.Stmt) - if !ok { - return nil - } - ts := tx.tx.StmtContext(tx.c, as) - st := &Stmt{query: stmt.query, tx: true, t: tx.t, db: tx.db} - st.stmt.Store(ts) - return st -} - -// Stmts returns a transaction-specific prepared statement from an existing statement. -func (tx *Tx) Stmts(stmt *Stmts) *Stmt { - return tx.Stmt(stmt.prepare(tx.db)) -} - -// Prepare creates a prepared statement for use within a transaction. -// The returned statement operates within the transaction and can no longer be -// used once the transaction has been committed or rolled back. -// To use an existing prepared statement on this transaction, see Tx.Stmt. -func (tx *Tx) Prepare(query string) (*Stmt, error) { - if tx.t != nil { - tx.t.SetTag(trace.String(trace.TagAnnotation, fmt.Sprintf("prepare %s", query))) - } - defer slowLog(fmt.Sprintf("Prepare addr: %s query(%s)", tx.db.addr, query), time.Now()) - stmt, err := tx.tx.Prepare(query) - if err != nil { - err = errors.Wrapf(err, "addr: %s prepare %s", tx.db.addr, query) - return nil, err - } - st := &Stmt{query: query, tx: true, t: tx.t, db: tx.db} - st.stmt.Store(stmt) - return st, nil -} - -// parseDSNAddr parse dsn name and return addr. -func parseDSNAddr(dsn string) (addr string) { - if dsn == "" { - return - } - cfg, err := mysql.ParseDSN(dsn) - if err != nil { - return - } - addr = cfg.Addr - return -} - -func genDSN(dsn, addr string) (res string) { - cfg, err := mysql.ParseDSN(dsn) - if err != nil { - return - } - cfg.Addr = addr - cfg.Net = "tcp" - res = cfg.FormatDSN() - return -} - -func slowLog(statement string, now time.Time) { - du := time.Since(now) - if du > _slowLogDuration { - log.Warn("%s slow log statement: %s time: %v", _family, statement, du) - } -} diff --git a/pkg/database/tidb/tidb.go b/pkg/database/tidb/tidb.go deleted file mode 100644 index 02fbe8731..000000000 --- a/pkg/database/tidb/tidb.go +++ /dev/null @@ -1,35 +0,0 @@ -package tidb - -import ( - "github.com/go-kratos/kratos/pkg/log" - "github.com/go-kratos/kratos/pkg/net/netutil/breaker" - "github.com/go-kratos/kratos/pkg/time" - - // database driver - _ "github.com/go-sql-driver/mysql" -) - -// Config mysql config. -type Config struct { - DSN string // dsn - Active int // pool - Idle int // pool - IdleTimeout time.Duration // connect max life time. - QueryTimeout time.Duration // query sql timeout - ExecTimeout time.Duration // execute sql timeout - TranTimeout time.Duration // transaction sql timeout - Breaker *breaker.Config // breaker -} - -// NewTiDB new db and retry connection when has error. -func NewTiDB(c *Config) (db *DB) { - if c.QueryTimeout == 0 || c.ExecTimeout == 0 || c.TranTimeout == 0 { - panic("tidb must be set query/execute/transction timeout") - } - db, err := Open(c) - if err != nil { - log.Error("open tidb error(%v)", err) - panic(err) - } - return -} diff --git a/pkg/ecode/common_ecode.go b/pkg/ecode/common_ecode.go deleted file mode 100644 index d28169c22..000000000 --- a/pkg/ecode/common_ecode.go +++ /dev/null @@ -1,20 +0,0 @@ -package ecode - -// All common ecode -var ( - OK = add(0) // 正确 - - NotModified = add(-304) // 木有改动 - TemporaryRedirect = add(-307) // 撞车跳转 - RequestErr = add(-400) // 请求错误 - Unauthorized = add(-401) // 未认证 - AccessDenied = add(-403) // 访问权限不足 - NothingFound = add(-404) // 啥都木有 - MethodNotAllowed = add(-405) // 不支持该方法 - Conflict = add(-409) // 冲突 - Canceled = add(-498) // 客户端取消请求 - ServerErr = add(-500) // 服务器错误 - ServiceUnavailable = add(-503) // 过载保护,服务暂不可用 - Deadline = add(-504) // 服务调用超时 - LimitExceed = add(-509) // 超出限制 -) diff --git a/pkg/ecode/ecode.go b/pkg/ecode/ecode.go deleted file mode 100644 index 259424cf4..000000000 --- a/pkg/ecode/ecode.go +++ /dev/null @@ -1,116 +0,0 @@ -package ecode - -import ( - "fmt" - "strconv" - "sync/atomic" - - "github.com/pkg/errors" -) - -var ( - _messages atomic.Value // NOTE: stored map[int]string - _codes = map[int]struct{}{} // register codes. -) - -// Register register ecode message map. -func Register(cm map[int]string) { - _messages.Store(cm) -} - -// New new a ecode.Codes by int value. -// NOTE: ecode must unique in global, the New will check repeat and then panic. -func New(e int) Code { - if e <= 0 { - panic("business ecode must greater than zero") - } - return add(e) -} - -func add(e int) Code { - if _, ok := _codes[e]; ok { - panic(fmt.Sprintf("ecode: %d already exist", e)) - } - _codes[e] = struct{}{} - return Int(e) -} - -// Codes ecode error interface which has a code & message. -type Codes interface { - // sometimes Error return Code in string form - // NOTE: don't use Error in monitor report even it also work for now - Error() string - // Code get error code. - Code() int - // Message get code message. - Message() string - //Detail get error detail,it may be nil. - Details() []interface{} -} - -// A Code is an int error code spec. -type Code int - -func (e Code) Error() string { - return strconv.FormatInt(int64(e), 10) -} - -// Code return error code -func (e Code) Code() int { return int(e) } - -// Message return error message -func (e Code) Message() string { - if cm, ok := _messages.Load().(map[int]string); ok { - if msg, ok := cm[e.Code()]; ok { - return msg - } - } - return e.Error() -} - -// Details return details. -func (e Code) Details() []interface{} { return nil } - -// Int parse code int to error. -func Int(i int) Code { return Code(i) } - -// String parse code string to error. -func String(e string) Code { - if e == "" { - return OK - } - // try error string - i, err := strconv.Atoi(e) - if err != nil { - return ServerErr - } - return Code(i) -} - -// Cause cause from error to ecode. -func Cause(e error) Codes { - if e == nil { - return OK - } - ec, ok := errors.Cause(e).(Codes) - if ok { - return ec - } - return String(e.Error()) -} - -// Equal equal a and b by code int. -func Equal(a, b Codes) bool { - if a == nil { - a = OK - } - if b == nil { - b = OK - } - return a.Code() == b.Code() -} - -// EqualError equal error -func EqualError(code Codes, err error) bool { - return Cause(err).Code() == code.Code() -} diff --git a/pkg/ecode/status.go b/pkg/ecode/status.go deleted file mode 100644 index 78fdf1e3d..000000000 --- a/pkg/ecode/status.go +++ /dev/null @@ -1,98 +0,0 @@ -package ecode - -import ( - "fmt" - "strconv" - - "github.com/go-kratos/kratos/pkg/ecode/types" - - "github.com/golang/protobuf/proto" - "github.com/golang/protobuf/ptypes" -) - -// Error new status with code and message -func Error(code Code, message string) *Status { - return &Status{s: &types.Status{Code: int32(code.Code()), Message: message}} -} - -// Errorf new status with code and message -func Errorf(code Code, format string, args ...interface{}) *Status { - return Error(code, fmt.Sprintf(format, args...)) -} - -var _ Codes = &Status{} - -// Status statusError is an alias of a status proto -// implement ecode.Codes -type Status struct { - s *types.Status -} - -// Error implement error -func (s *Status) Error() string { - return s.Message() -} - -// Code return error code -func (s *Status) Code() int { - return int(s.s.Code) -} - -// Message return error message for developer -func (s *Status) Message() string { - if s.s.Message == "" { - return strconv.Itoa(int(s.s.Code)) - } - return s.s.Message -} - -// Details return error details -func (s *Status) Details() []interface{} { - if s == nil || s.s == nil { - return nil - } - details := make([]interface{}, 0, len(s.s.Details)) - for _, any := range s.s.Details { - detail := &ptypes.DynamicAny{} - if err := ptypes.UnmarshalAny(any, detail); err != nil { - details = append(details, err) - continue - } - details = append(details, detail.Message) - } - return details -} - -// WithDetails WithDetails -func (s *Status) WithDetails(pbs ...proto.Message) (*Status, error) { - for _, pb := range pbs { - anyMsg, err := ptypes.MarshalAny(pb) - if err != nil { - return s, err - } - s.s.Details = append(s.s.Details, anyMsg) - } - return s, nil -} - -// Proto return origin protobuf message -func (s *Status) Proto() *types.Status { - return s.s -} - -// FromCode create status from ecode -func FromCode(code Code) *Status { - return &Status{s: &types.Status{Code: int32(code), Message: code.Message()}} -} - -// FromProto new status from grpc detail -func FromProto(pbMsg proto.Message) Codes { - if msg, ok := pbMsg.(*types.Status); ok { - if msg.Message == "" || msg.Message == strconv.FormatInt(int64(msg.Code), 10) { - // NOTE: if message is empty convert to pure Code, will get message from config center. - return Code(msg.Code) - } - return &Status{s: msg} - } - return Errorf(ServerErr, "invalid proto message get %v", pbMsg) -} diff --git a/pkg/ecode/status_test.go b/pkg/ecode/status_test.go deleted file mode 100644 index 5778ac4be..000000000 --- a/pkg/ecode/status_test.go +++ /dev/null @@ -1,57 +0,0 @@ -package ecode - -import ( - "testing" - "time" - - "github.com/golang/protobuf/ptypes/timestamp" - "github.com/stretchr/testify/assert" - - "github.com/go-kratos/kratos/pkg/ecode/types" -) - -func TestEqual(t *testing.T) { - var ( - err1 = Error(RequestErr, "test") - err2 = Errorf(RequestErr, "test") - ) - assert.Equal(t, err1, err2) - assert.True(t, Equal(nil, nil)) -} - -func TestDetail(t *testing.T) { - m := ×tamp.Timestamp{Seconds: time.Now().Unix()} - st, _ := Error(RequestErr, "RequestErr").WithDetails(m) - - assert.Equal(t, "RequestErr", st.Message()) - assert.Equal(t, int(RequestErr), st.Code()) - assert.IsType(t, m, st.Details()[0]) -} - -func TestFromCode(t *testing.T) { - err := FromCode(RequestErr) - - assert.Equal(t, int(RequestErr), err.Code()) - assert.Equal(t, "-400", err.Message()) -} - -func TestFromProto(t *testing.T) { - msg := &types.Status{Code: 2233, Message: "error"} - err := FromProto(msg) - - assert.Equal(t, 2233, err.Code()) - assert.Equal(t, "error", err.Message()) - - m := ×tamp.Timestamp{Seconds: time.Now().Unix()} - err = FromProto(m) - assert.Equal(t, -500, err.Code()) - assert.Contains(t, err.Message(), "invalid proto message get") -} - -func TestEmpty(t *testing.T) { - st := &Status{} - assert.Len(t, st.Details(), 0) - - st = nil - assert.Len(t, st.Details(), 0) -} diff --git a/pkg/ecode/types/status.pb.go b/pkg/ecode/types/status.pb.go deleted file mode 100644 index 8fe2c79c6..000000000 --- a/pkg/ecode/types/status.pb.go +++ /dev/null @@ -1,102 +0,0 @@ -// Code generated by protoc-gen-go. DO NOT EDIT. -// source: internal/types/status.proto - -package types // import "github.com/go-kratos/kratos/pkg/ecode/types" - -import proto "github.com/golang/protobuf/proto" -import fmt "fmt" -import math "math" -import any "github.com/golang/protobuf/ptypes/any" - -// Reference imports to suppress errors if they are not otherwise used. -var _ = proto.Marshal -var _ = fmt.Errorf -var _ = math.Inf - -// This is a compile-time assertion to ensure that this generated file -// is compatible with the proto package it is being compiled against. -// A compilation error at this line likely means your copy of the -// proto package needs to be updated. -const _ = proto.ProtoPackageIsVersion2 // please upgrade the proto package - -type Status struct { - // The error code see ecode.Code - Code int32 `protobuf:"varint,1,opt,name=code" json:"code,omitempty"` - // A developer-facing error message, which should be in English. Any - Message string `protobuf:"bytes,2,opt,name=message" json:"message,omitempty"` - // A list of messages that carry the error details. There is a common set of - // message types for APIs to use. - Details []*any.Any `protobuf:"bytes,3,rep,name=details" json:"details,omitempty"` - XXX_NoUnkeyedLiteral struct{} `json:"-"` - XXX_unrecognized []byte `json:"-"` - XXX_sizecache int32 `json:"-"` -} - -func (m *Status) Reset() { *m = Status{} } -func (m *Status) String() string { return proto.CompactTextString(m) } -func (*Status) ProtoMessage() {} -func (*Status) Descriptor() ([]byte, []int) { - return fileDescriptor_status_88668d6b2bf80f08, []int{0} -} -func (m *Status) XXX_Unmarshal(b []byte) error { - return xxx_messageInfo_Status.Unmarshal(m, b) -} -func (m *Status) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { - return xxx_messageInfo_Status.Marshal(b, m, deterministic) -} -func (dst *Status) XXX_Merge(src proto.Message) { - xxx_messageInfo_Status.Merge(dst, src) -} -func (m *Status) XXX_Size() int { - return xxx_messageInfo_Status.Size(m) -} -func (m *Status) XXX_DiscardUnknown() { - xxx_messageInfo_Status.DiscardUnknown(m) -} - -var xxx_messageInfo_Status proto.InternalMessageInfo - -func (m *Status) GetCode() int32 { - if m != nil { - return m.Code - } - return 0 -} - -func (m *Status) GetMessage() string { - if m != nil { - return m.Message - } - return "" -} - -func (m *Status) GetDetails() []*any.Any { - if m != nil { - return m.Details - } - return nil -} - -func init() { - proto.RegisterType((*Status)(nil), "bilibili.rpc.Status") -} - -func init() { proto.RegisterFile("internal/types/status.proto", fileDescriptor_status_88668d6b2bf80f08) } - -var fileDescriptor_status_88668d6b2bf80f08 = []byte{ - // 220 bytes of a gzipped FileDescriptorProto - 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0x54, 0x8f, 0xb1, 0x4a, 0x04, 0x31, - 0x10, 0x86, 0xd9, 0x5b, 0xbd, 0xc3, 0x9c, 0x85, 0x04, 0x8b, 0x55, 0x9b, 0xc5, 0x6a, 0x0b, 0x4d, - 0x40, 0x4b, 0x2b, 0xcf, 0x17, 0x58, 0x22, 0x36, 0x76, 0x49, 0x6e, 0x2e, 0x04, 0x92, 0xcc, 0x92, - 0xe4, 0x8a, 0xbc, 0x8e, 0x4f, 0x2a, 0x9b, 0x65, 0x41, 0x8b, 0x19, 0x66, 0x98, 0xff, 0xe7, 0xfb, - 0x87, 0x3c, 0xd8, 0x90, 0x21, 0x06, 0xe9, 0x78, 0x2e, 0x13, 0x24, 0x9e, 0xb2, 0xcc, 0xe7, 0xc4, - 0xa6, 0x88, 0x19, 0xe9, 0xb5, 0xb2, 0xce, 0xce, 0xc5, 0xe2, 0xa4, 0xef, 0xef, 0x0c, 0xa2, 0x71, - 0xc0, 0xeb, 0x4d, 0x9d, 0x4f, 0x5c, 0x86, 0xb2, 0x08, 0x1f, 0x4f, 0x64, 0xfb, 0x59, 0x8d, 0x94, - 0x92, 0x0b, 0x8d, 0x47, 0xe8, 0x9a, 0xbe, 0x19, 0x2e, 0x45, 0x9d, 0x69, 0x47, 0x76, 0x1e, 0x52, - 0x92, 0x06, 0xba, 0x4d, 0xdf, 0x0c, 0x57, 0x62, 0x5d, 0x29, 0x23, 0xbb, 0x23, 0x64, 0x69, 0x5d, - 0xea, 0xda, 0xbe, 0x1d, 0xf6, 0x2f, 0xb7, 0x6c, 0x81, 0xb0, 0x15, 0xc2, 0xde, 0x43, 0x11, 0xab, - 0xe8, 0xf0, 0x45, 0x6e, 0x34, 0x7a, 0xf6, 0x37, 0xd6, 0x61, 0xbf, 0x90, 0xc7, 0xd9, 0x30, 0x36, - 0xdf, 0x4f, 0x06, 0x9f, 0x35, 0x7a, 0x8f, 0x81, 0x3b, 0xab, 0xa2, 0x8c, 0x85, 0xc3, 0x9c, 0x82, - 0xff, 0x7f, 0xf4, 0xad, 0xf6, 0x9f, 0x4d, 0x2b, 0xc6, 0x0f, 0xb5, 0xad, 0xb4, 0xd7, 0xdf, 0x00, - 0x00, 0x00, 0xff, 0xff, 0x80, 0xa3, 0xc1, 0x82, 0x0d, 0x01, 0x00, 0x00, -} diff --git a/pkg/ecode/types/status.proto b/pkg/ecode/types/status.proto deleted file mode 100644 index 86101fc1d..000000000 --- a/pkg/ecode/types/status.proto +++ /dev/null @@ -1,23 +0,0 @@ -syntax = "proto3"; - -package bilibili.rpc; - -import "google/protobuf/any.proto"; - -option go_package = "github.com/go-kratos/kratos/pkg/ecode/types;types"; -option java_multiple_files = true; -option java_outer_classname = "StatusProto"; -option java_package = "com.bilibili.rpc"; -option objc_class_prefix = "RPC"; - -message Status { - // The error code see ecode.Code - int32 code = 1; - - // A developer-facing error message, which should be in English. Any - string message = 2; - - // A list of messages that carry the error details. There is a common set of - // message types for APIs to use. - repeated google.protobuf.Any details = 3; -} diff --git a/pkg/log/doc.go b/pkg/log/doc.go deleted file mode 100644 index 8c85fe52e..000000000 --- a/pkg/log/doc.go +++ /dev/null @@ -1,69 +0,0 @@ -/*Package log 是kratos日志库. - -一、主要功能: - - 1. 日志打印到elk - 2. 日志打印到本地,内部使用log4go - 3. 日志打印到标准输出 - 4. verbose日志实现,参考glog实现,可通过设置不同verbose级别,默认不开启 - -二、日志配置 - -1. 默认agent配置 - - 目前日志已经实现默认配置,可以根据env自动切换远程日志。可以直接使用以下方式: - log.Init(nil) - -2. 启动参数 or 环境变量 - - 启动参数 环境变量 说明 - log.stdout LOG_STDOUT 是否开启标准输出 - log.agent LOG_AGENT 远端日志地址:unixpacket:///var/run/lancer/collector_tcp.sock?timeout=100ms&chan=1024 - log.dir LOG_DIR 文件日志路径 - log.v LOG_V verbose日志级别 - log.module LOG_MODULE 可单独配置每个文件的verbose级别:file=1,file2=2 - log.filter LOG_FILTER 配置需要过滤的字段:field1,field2 - -3. 配置文件 -但是如果有特殊需要可以走一下格式配置: - [log] - family = "xxx-service" - dir = "/data/log/xxx-service/" - stdout = true - vLevel = 3 - filter = ["fileld1", "field2"] - [log.module] - "dao_user" = 2 - "servic*" = 1 - [log.agent] - taskID = "00000x" - proto = "unixpacket" - addr = "/var/run/lancer/collector_tcp.sock" - chanSize = 10240 - -三、配置说明 - -1.log - - family 项目名,默认读环境变量$APPID - studout 标准输出,prod环境不建议开启 - filter 配置需要过滤掉的字段,以“***”替换 - dir 文件日志地址,prod环境不建议开启 - v 开启verbose级别日志,可指定全局级别 - -2. log.module - - 可单独配置每个文件的verbose级别 - -3. log.agent -远端日志配置项 - taskID lancer分配的taskID - proto 网络协议,常见:tcp, udp, unixgram - addr 网络地址,常见:ip:prot, sock - chanSize 日志队列长度 - -四、最佳实践 - -1. KVString 使用 KVString 代替 KV 可以减少对象分配, 避免给 golang GC 造成压力. -*/ -package log diff --git a/pkg/log/dsn.go b/pkg/log/dsn.go deleted file mode 100644 index ea8158a84..000000000 --- a/pkg/log/dsn.go +++ /dev/null @@ -1,48 +0,0 @@ -package log - -import ( - "fmt" - "strconv" - "strings" -) - -type verboseModule map[string]int32 - -type logFilter []string - -func (f *logFilter) String() string { - return fmt.Sprint(*f) -} - -// Set sets the value of the named command-line flag. -// format: -log.filter key1,key2 -func (f *logFilter) Set(value string) error { - for _, i := range strings.Split(value, ",") { - *f = append(*f, strings.TrimSpace(i)) - } - return nil -} - -func (m verboseModule) String() string { - var b strings.Builder - for k, v := range m { - b.WriteString(k) - b.WriteString(strconv.FormatInt(int64(v), 10)) - b.WriteString(",") - } - return b.String() -} - -// Set sets the value of the named command-line flag. -// format: -log.module file=1,file2=2 -func (m verboseModule) Set(value string) error { - for _, i := range strings.Split(value, ",") { - kv := strings.Split(i, "=") - if len(kv) == 2 { - if v, err := strconv.ParseInt(kv[1], 10, 64); err == nil { - m[strings.TrimSpace(kv[0])] = int32(v) - } - } - } - return nil -} diff --git a/pkg/log/field.go b/pkg/log/field.go deleted file mode 100644 index 3feecb8a6..000000000 --- a/pkg/log/field.go +++ /dev/null @@ -1,58 +0,0 @@ -package log - -import ( - "math" - "time" - - "github.com/go-kratos/kratos/pkg/log/internal/core" -) - -// D represents a map of entry level data used for structured logging. -// type D map[string]interface{} -type D = core.Field - -// KVString construct Field with string value. -func KVString(key string, value string) D { - return D{Key: key, Type: core.StringType, StringVal: value} -} - -// KVInt construct Field with int value. -func KVInt(key string, value int) D { - return D{Key: key, Type: core.IntTpye, Int64Val: int64(value)} -} - -// KVInt64 construct D with int64 value. -func KVInt64(key string, value int64) D { - return D{Key: key, Type: core.Int64Type, Int64Val: value} -} - -// KVUint construct Field with uint value. -func KVUint(key string, value uint) D { - return D{Key: key, Type: core.UintType, Int64Val: int64(value)} -} - -// KVUint64 construct Field with uint64 value. -func KVUint64(key string, value uint64) D { - return D{Key: key, Type: core.Uint64Type, Int64Val: int64(value)} -} - -// KVFloat32 construct Field with float32 value. -func KVFloat32(key string, value float32) D { - return D{Key: key, Type: core.Float32Type, Int64Val: int64(math.Float32bits(value))} -} - -// KVFloat64 construct Field with float64 value. -func KVFloat64(key string, value float64) D { - return D{Key: key, Type: core.Float64Type, Int64Val: int64(math.Float64bits(value))} -} - -// KVDuration construct Field with Duration value. -func KVDuration(key string, value time.Duration) D { - return D{Key: key, Type: core.DurationType, Int64Val: int64(value)} -} - -// KV return a log kv for logging field. -// NOTE: use KV{type name} can avoid object alloc and get better performance. []~( ̄▽ ̄)~*干杯 -func KV(key string, value interface{}) D { - return D{Key: key, Value: value} -} diff --git a/pkg/log/file.go b/pkg/log/file.go deleted file mode 100644 index 173fdab97..000000000 --- a/pkg/log/file.go +++ /dev/null @@ -1,97 +0,0 @@ -package log - -import ( - "context" - "io" - "path/filepath" - "time" - - "github.com/go-kratos/kratos/pkg/log/internal/filewriter" -) - -// level idx -const ( - _debugIdx = iota - _infoIdx - _warnIdx - _errorIdx - _fatalIdx - _totalIdx -) - -var _fileNames = map[int]string{ - _debugIdx: "debug.log", - _infoIdx: "info.log", - _warnIdx: "warning.log", - _errorIdx: "error.log", - _fatalIdx: "fatal.log", -} - -// FileHandler . -type FileHandler struct { - render Render - fws [_totalIdx]*filewriter.FileWriter -} - -// NewFile crete a file logger. -func NewFile(dir string, bufferSize, rotateSize int64, maxLogFile int) *FileHandler { - // new info writer - newWriter := func(name string) *filewriter.FileWriter { - var options []filewriter.Option - if rotateSize > 0 { - options = append(options, filewriter.MaxSize(rotateSize)) - } - if maxLogFile > 0 { - options = append(options, filewriter.MaxFile(maxLogFile)) - } - w, err := filewriter.New(filepath.Join(dir, name), options...) - if err != nil { - panic(err) - } - return w - } - handler := &FileHandler{ - render: newPatternRender("[%D %T] [%L] [%S] %M"), - } - for idx, name := range _fileNames { - handler.fws[idx] = newWriter(name) - } - return handler -} - -// Log loggint to file . -func (h *FileHandler) Log(ctx context.Context, lv Level, args ...D) { - d := toMap(args...) - // add extra fields - addExtraField(ctx, d) - d[_time] = time.Now().Format(_timeFormat) - var w io.Writer - switch lv { - case _debugLevel: - w = h.fws[_debugIdx] - case _warnLevel: - w = h.fws[_warnIdx] - case _errorLevel: - w = h.fws[_errorIdx] - case _fatalLevel: - w = h.fws[_fatalIdx] - default: - w = h.fws[_infoIdx] - } - h.render.Render(w, d) - w.Write([]byte("\n")) -} - -// Close log handler -func (h *FileHandler) Close() error { - for _, fw := range h.fws { - // ignored error - fw.Close() - } - return nil -} - -// SetFormat set log format -func (h *FileHandler) SetFormat(format string) { - h.render = newPatternRender(format) -} diff --git a/pkg/log/handler.go b/pkg/log/handler.go deleted file mode 100644 index 71b846bf1..000000000 --- a/pkg/log/handler.go +++ /dev/null @@ -1,118 +0,0 @@ -package log - -import ( - "context" - "time" - - pkgerr "github.com/pkg/errors" -) - -const ( - _timeFormat = "2006-01-02T15:04:05.999999" - - // log level defined in level.go. - _levelValue = "level_value" - // log level name: INFO, WARN... - _level = "level" - // log time. - _time = "time" - // request path. - // _title = "title" - // log file. - _source = "source" - // common log filed. - _log = "log" - // app name. - _appID = "app_id" - // container ID. - _instanceID = "instance_id" - // uniq ID from trace. - _tid = "traceid" - // request time. - // _ts = "ts" - // requester. - _caller = "caller" - // container environment: prod, pre, uat, fat. - _deplyEnv = "env" - // container area. - _zone = "zone" - // mirror flag - _mirror = "mirror" - // color. - _color = "color" - // env_color - _envColor = "env_color" - // cluster. - _cluster = "cluster" -) - -// Handler is used to handle log events, outputting them to -// stdio or sending them to remote services. See the "handlers" -// directory for implementations. -// -// It is left up to Handlers to implement thread-safety. -type Handler interface { - // Log handle log - // variadic D is k-v struct represent log content - Log(context.Context, Level, ...D) - - // SetFormat set render format on log output - // see StdoutHandler.SetFormat for detail - SetFormat(string) - - // Close handler - Close() error -} - -func newHandlers(filters []string, handlers ...Handler) *Handlers { - set := make(map[string]struct{}) - for _, k := range filters { - set[k] = struct{}{} - } - return &Handlers{filters: set, handlers: handlers} -} - -// Handlers a bundle for hander with filter function. -type Handlers struct { - filters map[string]struct{} - handlers []Handler -} - -// Log handlers logging. -func (hs Handlers) Log(ctx context.Context, lv Level, d ...D) { - hasSource := false - for i := range d { - if _, ok := hs.filters[d[i].Key]; ok { - d[i].Value = "***" - } - if d[i].Key == _source { - hasSource = true - } - } - if !hasSource { - fn := funcName(3) - errIncr(lv, fn) - d = append(d, KVString(_source, fn)) - } - d = append(d, KV(_time, time.Now()), KVInt64(_levelValue, int64(lv)), KVString(_level, lv.String())) - for _, h := range hs.handlers { - h.Log(ctx, lv, d...) - } -} - -// Close close resource. -func (hs Handlers) Close() (err error) { - for _, h := range hs.handlers { - if e := h.Close(); e != nil { - err = pkgerr.WithStack(e) - } - } - return -} - -// SetFormat . -func (hs Handlers) SetFormat(format string) { - for _, h := range hs.handlers { - h.SetFormat(format) - } -} diff --git a/pkg/log/internal/LICENSE.txt b/pkg/log/internal/LICENSE.txt deleted file mode 100644 index 8ed1527bd..000000000 --- a/pkg/log/internal/LICENSE.txt +++ /dev/null @@ -1,21 +0,0 @@ -COPY FROM: https://github.com/uber-go/zap - -Copyright (c) 2016-2017 Uber Technologies, Inc. - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -THE SOFTWARE. \ No newline at end of file diff --git a/pkg/log/internal/core/buffer.go b/pkg/log/internal/core/buffer.go deleted file mode 100644 index a2efea492..000000000 --- a/pkg/log/internal/core/buffer.go +++ /dev/null @@ -1,97 +0,0 @@ -package core - -import "strconv" - -const _size = 1024 // by default, create 1 KiB buffers - -// NewBuffer is new buffer -func NewBuffer(_size int) *Buffer { - return &Buffer{bs: make([]byte, 0, _size)} -} - -// Buffer is a thin wrapper around a byte slice. It's intended to be pooled, so -// the only way to construct one is via a Pool. -type Buffer struct { - bs []byte - pool Pool -} - -// AppendByte writes a single byte to the Buffer. -func (b *Buffer) AppendByte(v byte) { - b.bs = append(b.bs, v) -} - -// AppendString writes a string to the Buffer. -func (b *Buffer) AppendString(s string) { - b.bs = append(b.bs, s...) -} - -// AppendInt appends an integer to the underlying buffer (assuming base 10). -func (b *Buffer) AppendInt(i int64) { - b.bs = strconv.AppendInt(b.bs, i, 10) -} - -// AppendUint appends an unsigned integer to the underlying buffer (assuming -// base 10). -func (b *Buffer) AppendUint(i uint64) { - b.bs = strconv.AppendUint(b.bs, i, 10) -} - -// AppendBool appends a bool to the underlying buffer. -func (b *Buffer) AppendBool(v bool) { - b.bs = strconv.AppendBool(b.bs, v) -} - -// AppendFloat appends a float to the underlying buffer. It doesn't quote NaN -// or +/- Inf. -func (b *Buffer) AppendFloat(f float64, bitSize int) { - b.bs = strconv.AppendFloat(b.bs, f, 'f', -1, bitSize) -} - -// Len returns the length of the underlying byte slice. -func (b *Buffer) Len() int { - return len(b.bs) -} - -// Cap returns the capacity of the underlying byte slice. -func (b *Buffer) Cap() int { - return cap(b.bs) -} - -// Bytes returns a mutable reference to the underlying byte slice. -func (b *Buffer) Bytes() []byte { - return b.bs -} - -// String returns a string copy of the underlying byte slice. -func (b *Buffer) String() string { - return string(b.bs) -} - -// Reset resets the underlying byte slice. Subsequent writes re-use the slice's -// backing array. -func (b *Buffer) Reset() { - b.bs = b.bs[:0] -} - -// Write implements io.Writer. -func (b *Buffer) Write(bs []byte) (int, error) { - b.bs = append(b.bs, bs...) - return len(bs), nil -} - -// TrimNewline trims any final "\n" byte from the end of the buffer. -func (b *Buffer) TrimNewline() { - if i := len(b.bs) - 1; i >= 0 { - if b.bs[i] == '\n' { - b.bs = b.bs[:i] - } - } -} - -// Free returns the Buffer to its Pool. -// -// Callers must not retain references to the Buffer after calling Free. -func (b *Buffer) Free() { - b.pool.put(b) -} diff --git a/pkg/log/internal/core/buffer_test.go b/pkg/log/internal/core/buffer_test.go deleted file mode 100644 index 67e723c0a..000000000 --- a/pkg/log/internal/core/buffer_test.go +++ /dev/null @@ -1,91 +0,0 @@ -// Copyright (c) 2016 Uber Technologies, Inc. -// -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the "Software"), to deal -// in the Software without restriction, including without limitation the rights -// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -// copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: -// -// The above copyright notice and this permission notice shall be included in -// all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -// THE SOFTWARE. - -package core - -import ( - "bytes" - "strings" - "testing" - - "github.com/stretchr/testify/assert" -) - -func TestBufferWrites(t *testing.T) { - buf := NewPool(0).Get() - - tests := []struct { - desc string - f func() - want string - }{ - {"AppendByte", func() { buf.AppendByte('v') }, "v"}, - {"AppendString", func() { buf.AppendString("foo") }, "foo"}, - {"AppendIntPositive", func() { buf.AppendInt(42) }, "42"}, - {"AppendIntNegative", func() { buf.AppendInt(-42) }, "-42"}, - {"AppendUint", func() { buf.AppendUint(42) }, "42"}, - {"AppendBool", func() { buf.AppendBool(true) }, "true"}, - {"AppendFloat64", func() { buf.AppendFloat(3.14, 64) }, "3.14"}, - // Intenationally introduce some floating-point error. - {"AppendFloat32", func() { buf.AppendFloat(float64(float32(3.14)), 32) }, "3.14"}, - {"AppendWrite", func() { buf.Write([]byte("foo")) }, "foo"}, - } - - for _, tt := range tests { - t.Run(tt.desc, func(t *testing.T) { - buf.Reset() - tt.f() - assert.Equal(t, tt.want, buf.String(), "Unexpected buffer.String().") - assert.Equal(t, tt.want, string(buf.Bytes()), "Unexpected string(buffer.Bytes()).") - assert.Equal(t, len(tt.want), buf.Len(), "Unexpected buffer length.") - // We're not writing more than a kibibyte in tests. - assert.Equal(t, _size, buf.Cap(), "Expected buffer capacity to remain constant.") - }) - } -} - -func BenchmarkBuffers(b *testing.B) { - // Because we use the strconv.AppendFoo functions so liberally, we can't - // use the standard library's bytes.Buffer anyways (without incurring a - // bunch of extra allocations). Nevertheless, let's make sure that we're - // not losing any precious nanoseconds. - str := strings.Repeat("a", 1024) - slice := make([]byte, 1024) - buf := bytes.NewBuffer(slice) - custom := NewPool(0).Get() - b.Run("ByteSlice", func(b *testing.B) { - for i := 0; i < b.N; i++ { - slice = append(slice, str...) - slice = slice[:0] - } - }) - b.Run("BytesBuffer", func(b *testing.B) { - for i := 0; i < b.N; i++ { - buf.WriteString(str) - buf.Reset() - } - }) - b.Run("CustomBuffer", func(b *testing.B) { - for i := 0; i < b.N; i++ { - custom.AppendString(str) - custom.Reset() - } - }) -} diff --git a/pkg/log/internal/core/bufferpool.go b/pkg/log/internal/core/bufferpool.go deleted file mode 100644 index d1ee3caaf..000000000 --- a/pkg/log/internal/core/bufferpool.go +++ /dev/null @@ -1,29 +0,0 @@ -// Copyright (c) 2016 Uber Technologies, Inc. -// -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the "Software"), to deal -// in the Software without restriction, including without limitation the rights -// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -// copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: -// -// The above copyright notice and this permission notice shall be included in -// all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -// THE SOFTWARE. - -// Package core houses zap's shared internal buffer pool. Third-party -// packages can recreate the same functionality with buffers.NewPool. -package core - -var ( - _pool = NewPool(_size) - // GetPool retrieves a buffer from the pool, creating one if necessary. - GetPool = _pool.Get -) diff --git a/pkg/log/internal/core/encoder.go b/pkg/log/internal/core/encoder.go deleted file mode 100644 index 912245244..000000000 --- a/pkg/log/internal/core/encoder.go +++ /dev/null @@ -1,187 +0,0 @@ -package core - -import ( - "time" -) - -// DefaultLineEnding defines the default line ending when writing logs. -// Alternate line endings specified in EncoderConfig can override this -// behavior. -const DefaultLineEnding = "\n" - -// ObjectEncoder is a strongly-typed, encoding-agnostic interface for adding a -// map- or struct-like object to the logging context. Like maps, ObjectEncoders -// aren't safe for concurrent use (though typical use shouldn't require locks). -type ObjectEncoder interface { - // Logging-specific marshalers. - AddArray(key string, marshaler ArrayMarshaler) error - AddObject(key string, marshaler ObjectMarshaler) error - - // Built-in types. - AddBinary(key string, value []byte) // for arbitrary bytes - AddByteString(key string, value []byte) // for UTF-8 encoded bytes - AddBool(key string, value bool) - AddComplex128(key string, value complex128) - AddComplex64(key string, value complex64) - AddDuration(key string, value time.Duration) - AddFloat64(key string, value float64) - AddFloat32(key string, value float32) - AddInt(key string, value int) - AddInt64(key string, value int64) - AddInt32(key string, value int32) - AddInt16(key string, value int16) - AddInt8(key string, value int8) - AddString(key, value string) - AddTime(key string, value time.Time) - AddUint(key string, value uint) - AddUint64(key string, value uint64) - AddUint32(key string, value uint32) - AddUint16(key string, value uint16) - AddUint8(key string, value uint8) - AddUintptr(key string, value uintptr) - - // AddReflected uses reflection to serialize arbitrary objects, so it's slow - // and allocation-heavy. - AddReflected(key string, value interface{}) error - // OpenNamespace opens an isolated namespace where all subsequent fields will - // be added. Applications can use namespaces to prevent key collisions when - // injecting loggers into sub-components or third-party libraries. - OpenNamespace(key string) -} - -// ObjectMarshaler allows user-defined types to efficiently add themselves to the -// logging context, and to selectively omit information which shouldn't be -// included in logs (e.g., passwords). -type ObjectMarshaler interface { - MarshalLogObject(ObjectEncoder) error -} - -// ObjectMarshalerFunc is a type adapter that turns a function into an -// ObjectMarshaler. -type ObjectMarshalerFunc func(ObjectEncoder) error - -// MarshalLogObject calls the underlying function. -func (f ObjectMarshalerFunc) MarshalLogObject(enc ObjectEncoder) error { - return f(enc) -} - -// ArrayMarshaler allows user-defined types to efficiently add themselves to the -// logging context, and to selectively omit information which shouldn't be -// included in logs (e.g., passwords). -type ArrayMarshaler interface { - MarshalLogArray(ArrayEncoder) error -} - -// ArrayMarshalerFunc is a type adapter that turns a function into an -// ArrayMarshaler. -type ArrayMarshalerFunc func(ArrayEncoder) error - -// MarshalLogArray calls the underlying function. -func (f ArrayMarshalerFunc) MarshalLogArray(enc ArrayEncoder) error { - return f(enc) -} - -// ArrayEncoder is a strongly-typed, encoding-agnostic interface for adding -// array-like objects to the logging context. Of note, it supports mixed-type -// arrays even though they aren't typical in Go. Like slices, ArrayEncoders -// aren't safe for concurrent use (though typical use shouldn't require locks). -type ArrayEncoder interface { - // Built-in types. - PrimitiveArrayEncoder - - // Time-related types. - AppendDuration(time.Duration) - AppendTime(time.Time) - - // Logging-specific marshalers. - AppendArray(ArrayMarshaler) error - AppendObject(ObjectMarshaler) error - - // AppendReflected uses reflection to serialize arbitrary objects, so it's - // slow and allocation-heavy. - AppendReflected(value interface{}) error -} - -// PrimitiveArrayEncoder is the subset of the ArrayEncoder interface that deals -// only in Go's built-in types. It's included only so that Duration- and -// TimeEncoders cannot trigger infinite recursion. -type PrimitiveArrayEncoder interface { - // Built-in types. - AppendBool(bool) - AppendByteString([]byte) // for UTF-8 encoded bytes - AppendComplex128(complex128) - AppendComplex64(complex64) - AppendFloat64(float64) - AppendFloat32(float32) - AppendInt(int) - AppendInt64(int64) - AppendInt32(int32) - AppendInt16(int16) - AppendInt8(int8) - AppendString(string) - AppendUint(uint) - AppendUint64(uint64) - AppendUint32(uint32) - AppendUint16(uint16) - AppendUint8(uint8) - AppendUintptr(uintptr) -} - -// An EncoderConfig allows users to configure the concrete encoders supplied by -// zapcore. -type EncoderConfig struct { - EncodeTime TimeEncoder `json:"timeEncoder" yaml:"timeEncoder"` - EncodeDuration DurationEncoder `json:"durationEncoder" yaml:"durationEncoder"` - // Configure the primitive representations of common complex types. For - // example, some users may want all time.Times serialized as floating-point - // seconds since epoch, while others may prefer ISO8601 strings. - /*EncodeLevel LevelEncoder `json:"levelEncoder" yaml:"levelEncoder"` - EncodeTime TimeEncoder `json:"timeEncoder" yaml:"timeEncoder"` - EncodeDuration DurationEncoder `json:"durationEncoder" yaml:"durationEncoder"` - EncodeCaller CallerEncoder `json:"callerEncoder" yaml:"callerEncoder"` - // Unlike the other primitive type encoders, EncodeName is optional. The - // zero value falls back to FullNameEncoder. - EncodeName NameEncoder `json:"nameEncoder" yaml:"nameEncoder"`*/ -} - -// Encoder is a format-agnostic interface for all log entry marshalers. Since -// log encoders don't need to support the same wide range of use cases as -// general-purpose marshalers, it's possible to make them faster and -// lower-allocation. -// -// Implementations of the ObjectEncoder interface's methods can, of course, -// freely modify the receiver. However, the Clone and EncodeEntry methods will -// be called concurrently and shouldn't modify the receiver. -type Encoder interface { - ObjectEncoder - - // Clone copies the encoder, ensuring that adding fields to the copy doesn't - // affect the original. - Clone() Encoder - - // EncodeEntry encodes an entry and fields, along with any accumulated - // context, into a byte buffer and returns it. - Encode(*Buffer, ...Field) error -} - -// A TimeEncoder serializes a time.Time to a primitive type. -type TimeEncoder func(time.Time, PrimitiveArrayEncoder) - -// A DurationEncoder serializes a time.Duration to a primitive type. -type DurationEncoder func(time.Duration, PrimitiveArrayEncoder) - -// EpochTimeEncoder serializes a time.Time to a floating-point number of seconds -// since the Unix epoch. -func EpochTimeEncoder(t time.Time, enc PrimitiveArrayEncoder) { - //var d []byte - enc.AppendString(t.Format("2006-01-02T15:04:05.999999")) - //enc.AppendByteString(t.AppendFormat(d, "2006-01-02T15:04:05.999999")) - /*nanos := t.UnixNano() - sec := float64(nanos) / float64(time.Second) - enc.AppendFloat64(sec)*/ -} - -// SecondsDurationEncoder serializes a time.Duration to a floating-point number of seconds elapsed. -func SecondsDurationEncoder(d time.Duration, enc PrimitiveArrayEncoder) { - enc.AppendFloat64(float64(d) / float64(time.Second)) -} diff --git a/pkg/log/internal/core/field.go b/pkg/log/internal/core/field.go deleted file mode 100644 index 66d1c5992..000000000 --- a/pkg/log/internal/core/field.go +++ /dev/null @@ -1,122 +0,0 @@ -package core - -import ( - "fmt" - "math" - "time" - - xtime "github.com/go-kratos/kratos/pkg/time" -) - -// FieldType represent D value type -type FieldType int32 - -// DType enum -const ( - UnknownType FieldType = iota - StringType - IntTpye - Int64Type - UintType - Uint64Type - Float32Type - Float64Type - DurationType -) - -// Field is for encoder -type Field struct { - Key string - Value interface{} - Type FieldType - StringVal string - Int64Val int64 -} - -// AddTo exports a field through the ObjectEncoder interface. It's primarily -// useful to library authors, and shouldn't be necessary in most applications. -func (f Field) AddTo(enc ObjectEncoder) { - if f.Type == UnknownType { - f.assertAddTo(enc) - return - } - switch f.Type { - case StringType: - enc.AddString(f.Key, f.StringVal) - case IntTpye: - enc.AddInt(f.Key, int(f.Int64Val)) - case Int64Type: - enc.AddInt64(f.Key, f.Int64Val) - case UintType: - enc.AddUint(f.Key, uint(f.Int64Val)) - case Uint64Type: - enc.AddUint64(f.Key, uint64(f.Int64Val)) - case Float32Type: - enc.AddFloat32(f.Key, math.Float32frombits(uint32(f.Int64Val))) - case Float64Type: - enc.AddFloat64(f.Key, math.Float64frombits(uint64(f.Int64Val))) - case DurationType: - enc.AddDuration(f.Key, time.Duration(f.Int64Val)) - default: - panic(fmt.Sprintf("unknown field type: %v", f)) - } -} - -func (f Field) assertAddTo(enc ObjectEncoder) { - // assert interface - switch val := f.Value.(type) { - case bool: - enc.AddBool(f.Key, val) - case complex128: - enc.AddComplex128(f.Key, val) - case complex64: - enc.AddComplex64(f.Key, val) - case float64: - enc.AddFloat64(f.Key, val) - case float32: - enc.AddFloat32(f.Key, val) - case int: - enc.AddInt(f.Key, val) - case int64: - enc.AddInt64(f.Key, val) - case int32: - enc.AddInt32(f.Key, val) - case int16: - enc.AddInt16(f.Key, val) - case int8: - enc.AddInt8(f.Key, val) - case string: - enc.AddString(f.Key, val) - case uint: - enc.AddUint(f.Key, val) - case uint64: - enc.AddUint64(f.Key, val) - case uint32: - enc.AddUint32(f.Key, val) - case uint16: - enc.AddUint16(f.Key, val) - case uint8: - enc.AddUint8(f.Key, val) - case []byte: - enc.AddByteString(f.Key, val) - case uintptr: - enc.AddUintptr(f.Key, val) - case time.Time: - enc.AddTime(f.Key, val) - case xtime.Time: - enc.AddTime(f.Key, val.Time()) - case time.Duration: - enc.AddDuration(f.Key, val) - case xtime.Duration: - enc.AddDuration(f.Key, time.Duration(val)) - case error: - enc.AddString(f.Key, val.Error()) - case fmt.Stringer: - enc.AddString(f.Key, val.String()) - default: - err := enc.AddReflected(f.Key, val) - if err != nil { - enc.AddString(fmt.Sprintf("%sError", f.Key), err.Error()) - } - } -} diff --git a/pkg/log/internal/core/json_encoder.go b/pkg/log/internal/core/json_encoder.go deleted file mode 100644 index 1f9dadc20..000000000 --- a/pkg/log/internal/core/json_encoder.go +++ /dev/null @@ -1,424 +0,0 @@ -package core - -import ( - "encoding/base64" - "encoding/json" - "math" - "sync" - "time" - "unicode/utf8" -) - -// For JSON-escaping; see jsonEncoder.safeAddString below. -const _hex = "0123456789abcdef" - -var _ ObjectEncoder = &jsonEncoder{} -var _jsonPool = sync.Pool{New: func() interface{} { - return &jsonEncoder{} -}} - -func getJSONEncoder() *jsonEncoder { - return _jsonPool.Get().(*jsonEncoder) -} - -func putJSONEncoder(enc *jsonEncoder) { - if enc.reflectBuf != nil { - enc.reflectBuf.Free() - } - enc.EncoderConfig = nil - enc.buf = nil - enc.spaced = false - enc.openNamespaces = 0 - enc.reflectBuf = nil - enc.reflectEnc = nil - _jsonPool.Put(enc) -} - -type jsonEncoder struct { - *EncoderConfig - buf *Buffer - spaced bool // include spaces after colons and commas - openNamespaces int - - // for encoding generic values by reflection - reflectBuf *Buffer - reflectEnc *json.Encoder -} - -// NewJSONEncoder creates a fast, low-allocation JSON encoder. The encoder -// appropriately escapes all field keys and values. -// -// Note that the encoder doesn't deduplicate keys, so it's possible to produce -// a message like -// {"foo":"bar","foo":"baz"} -// This is permitted by the JSON specification, but not encouraged. Many -// libraries will ignore duplicate key-value pairs (typically keeping the last -// pair) when unmarshaling, but users should attempt to avoid adding duplicate -// keys. -func NewJSONEncoder(cfg EncoderConfig, buf *Buffer) Encoder { - return newJSONEncoder(cfg, false, buf) -} - -func newJSONEncoder(cfg EncoderConfig, spaced bool, buf *Buffer) *jsonEncoder { - return &jsonEncoder{ - EncoderConfig: &cfg, - buf: buf, - spaced: spaced, - } -} - -func (enc *jsonEncoder) AddArray(key string, arr ArrayMarshaler) error { - enc.addKey(key) - return enc.AppendArray(arr) -} - -func (enc *jsonEncoder) AddObject(key string, obj ObjectMarshaler) error { - enc.addKey(key) - return enc.AppendObject(obj) -} - -func (enc *jsonEncoder) AddBinary(key string, val []byte) { - enc.AddString(key, base64.StdEncoding.EncodeToString(val)) -} - -func (enc *jsonEncoder) AddByteString(key string, val []byte) { - enc.addKey(key) - enc.AppendByteString(val) -} - -func (enc *jsonEncoder) AddBool(key string, val bool) { - enc.addKey(key) - enc.AppendBool(val) -} - -func (enc *jsonEncoder) AddComplex128(key string, val complex128) { - enc.addKey(key) - enc.AppendComplex128(val) -} - -func (enc *jsonEncoder) AddDuration(key string, val time.Duration) { - enc.addKey(key) - enc.AppendDuration(val) -} - -func (enc *jsonEncoder) AddFloat64(key string, val float64) { - enc.addKey(key) - enc.AppendFloat64(val) -} - -func (enc *jsonEncoder) AddInt64(key string, val int64) { - enc.addKey(key) - enc.AppendInt64(val) -} - -func (enc *jsonEncoder) resetReflectBuf() { - if enc.reflectBuf == nil { - enc.reflectBuf = GetPool() - enc.reflectEnc = json.NewEncoder(enc.reflectBuf) - } else { - enc.reflectBuf.Reset() - } -} - -func (enc *jsonEncoder) AddReflected(key string, obj interface{}) error { - enc.resetReflectBuf() - err := enc.reflectEnc.Encode(obj) - if err != nil { - return err - } - enc.reflectBuf.TrimNewline() - enc.addKey(key) - _, err = enc.buf.Write(enc.reflectBuf.Bytes()) - return err -} - -func (enc *jsonEncoder) OpenNamespace(key string) { - enc.addKey(key) - enc.buf.AppendByte('{') - enc.openNamespaces++ -} - -func (enc *jsonEncoder) AddString(key, val string) { - enc.addKey(key) - enc.AppendString(val) -} - -func (enc *jsonEncoder) AddTime(key string, val time.Time) { - enc.addKey(key) - enc.AppendTime(val) -} - -func (enc *jsonEncoder) AddUint64(key string, val uint64) { - enc.addKey(key) - enc.AppendUint64(val) -} - -func (enc *jsonEncoder) AppendArray(arr ArrayMarshaler) error { - enc.addElementSeparator() - enc.buf.AppendByte('[') - err := arr.MarshalLogArray(enc) - enc.buf.AppendByte(']') - return err -} - -func (enc *jsonEncoder) AppendObject(obj ObjectMarshaler) error { - enc.addElementSeparator() - enc.buf.AppendByte('{') - err := obj.MarshalLogObject(enc) - enc.buf.AppendByte('}') - return err -} - -func (enc *jsonEncoder) AppendBool(val bool) { - enc.addElementSeparator() - enc.buf.AppendBool(val) -} - -func (enc *jsonEncoder) AppendByteString(val []byte) { - enc.addElementSeparator() - enc.buf.AppendByte('"') - enc.safeAddByteString(val) - enc.buf.AppendByte('"') -} - -func (enc *jsonEncoder) AppendComplex128(val complex128) { - enc.addElementSeparator() - // Cast to a platform-independent, fixed-size type. - r, i := float64(real(val)), float64(imag(val)) - enc.buf.AppendByte('"') - // Because we're always in a quoted string, we can use strconv without - // special-casing NaN and +/-Inf. - enc.buf.AppendFloat(r, 64) - enc.buf.AppendByte('+') - enc.buf.AppendFloat(i, 64) - enc.buf.AppendByte('i') - enc.buf.AppendByte('"') -} - -func (enc *jsonEncoder) AppendDuration(val time.Duration) { - cur := enc.buf.Len() - enc.EncodeDuration(val, enc) - if cur == enc.buf.Len() { - // User-supplied EncodeDuration is a no-op. Fall back to nanoseconds to keep - // JSON valid. - enc.AppendInt64(int64(val)) - } -} - -func (enc *jsonEncoder) AppendInt64(val int64) { - enc.addElementSeparator() - enc.buf.AppendInt(val) -} - -func (enc *jsonEncoder) AppendReflected(val interface{}) error { - enc.resetReflectBuf() - err := enc.reflectEnc.Encode(val) - if err != nil { - return err - } - enc.reflectBuf.TrimNewline() - enc.addElementSeparator() - _, err = enc.buf.Write(enc.reflectBuf.Bytes()) - return err -} - -func (enc *jsonEncoder) AppendString(val string) { - enc.addElementSeparator() - enc.buf.AppendByte('"') - enc.safeAddString(val) - enc.buf.AppendByte('"') -} - -func (enc *jsonEncoder) AppendTime(val time.Time) { - cur := enc.buf.Len() - enc.EncodeTime(val, enc) - if cur == enc.buf.Len() { - // User-supplied EncodeTime is a no-op. Fall back to nanos since epoch to keep - // output JSON valid. - enc.AppendInt64(val.UnixNano()) - } -} - -func (enc *jsonEncoder) AppendUint64(val uint64) { - enc.addElementSeparator() - enc.buf.AppendUint(val) -} - -func (enc *jsonEncoder) AddComplex64(k string, v complex64) { enc.AddComplex128(k, complex128(v)) } -func (enc *jsonEncoder) AddFloat32(k string, v float32) { enc.AddFloat64(k, float64(v)) } -func (enc *jsonEncoder) AddInt(k string, v int) { enc.AddInt64(k, int64(v)) } -func (enc *jsonEncoder) AddInt32(k string, v int32) { enc.AddInt64(k, int64(v)) } -func (enc *jsonEncoder) AddInt16(k string, v int16) { enc.AddInt64(k, int64(v)) } -func (enc *jsonEncoder) AddInt8(k string, v int8) { enc.AddInt64(k, int64(v)) } -func (enc *jsonEncoder) AddUint(k string, v uint) { enc.AddUint64(k, uint64(v)) } -func (enc *jsonEncoder) AddUint32(k string, v uint32) { enc.AddUint64(k, uint64(v)) } -func (enc *jsonEncoder) AddUint16(k string, v uint16) { enc.AddUint64(k, uint64(v)) } -func (enc *jsonEncoder) AddUint8(k string, v uint8) { enc.AddUint64(k, uint64(v)) } -func (enc *jsonEncoder) AddUintptr(k string, v uintptr) { enc.AddUint64(k, uint64(v)) } -func (enc *jsonEncoder) AppendComplex64(v complex64) { enc.AppendComplex128(complex128(v)) } -func (enc *jsonEncoder) AppendFloat64(v float64) { enc.appendFloat(v, 64) } -func (enc *jsonEncoder) AppendFloat32(v float32) { enc.appendFloat(float64(v), 32) } -func (enc *jsonEncoder) AppendInt(v int) { enc.AppendInt64(int64(v)) } -func (enc *jsonEncoder) AppendInt32(v int32) { enc.AppendInt64(int64(v)) } -func (enc *jsonEncoder) AppendInt16(v int16) { enc.AppendInt64(int64(v)) } -func (enc *jsonEncoder) AppendInt8(v int8) { enc.AppendInt64(int64(v)) } -func (enc *jsonEncoder) AppendUint(v uint) { enc.AppendUint64(uint64(v)) } -func (enc *jsonEncoder) AppendUint32(v uint32) { enc.AppendUint64(uint64(v)) } -func (enc *jsonEncoder) AppendUint16(v uint16) { enc.AppendUint64(uint64(v)) } -func (enc *jsonEncoder) AppendUint8(v uint8) { enc.AppendUint64(uint64(v)) } -func (enc *jsonEncoder) AppendUintptr(v uintptr) { enc.AppendUint64(uint64(v)) } - -func (enc *jsonEncoder) Clone() Encoder { - clone := enc.clone() - return clone -} - -func (enc *jsonEncoder) clone() *jsonEncoder { - clone := getJSONEncoder() - clone.EncoderConfig = enc.EncoderConfig - clone.spaced = enc.spaced - clone.openNamespaces = enc.openNamespaces - clone.buf = GetPool() - return clone -} - -func (enc *jsonEncoder) Encode(buf *Buffer, fields ...Field) error { - final := enc.clone() - final.buf = buf - final.buf.AppendByte('{') - if enc.buf.Len() > 0 { - final.addElementSeparator() - final.buf.Write(enc.buf.Bytes()) - } - - for i := range fields { - fields[i].AddTo(final) - } - - final.closeOpenNamespaces() - final.buf.AppendString("}\n") - putJSONEncoder(final) - return nil -} - -func (enc *jsonEncoder) closeOpenNamespaces() { - for i := 0; i < enc.openNamespaces; i++ { - enc.buf.AppendByte('}') - } -} - -func (enc *jsonEncoder) addKey(key string) { - enc.addElementSeparator() - enc.buf.AppendByte('"') - enc.safeAddString(key) - enc.buf.AppendByte('"') - enc.buf.AppendByte(':') - if enc.spaced { - enc.buf.AppendByte(' ') - } -} - -func (enc *jsonEncoder) addElementSeparator() { - last := enc.buf.Len() - 1 - if last < 0 { - return - } - switch enc.buf.Bytes()[last] { - case '{', '[', ':', ',', ' ': - return - default: - enc.buf.AppendByte(',') - if enc.spaced { - enc.buf.AppendByte(' ') - } - } -} - -func (enc *jsonEncoder) appendFloat(val float64, bitSize int) { - enc.addElementSeparator() - switch { - case math.IsNaN(val): - enc.buf.AppendString(`"NaN"`) - case math.IsInf(val, 1): - enc.buf.AppendString(`"+Inf"`) - case math.IsInf(val, -1): - enc.buf.AppendString(`"-Inf"`) - default: - enc.buf.AppendFloat(val, bitSize) - } -} - -// safeAddString JSON-escapes a string and appends it to the internal buffer. -// Unlike the standard library's encoder, it doesn't attempt to protect the -// user from browser vulnerabilities or JSONP-related problems. -func (enc *jsonEncoder) safeAddString(s string) { - for i := 0; i < len(s); { - if enc.tryAddRuneSelf(s[i]) { - i++ - continue - } - r, size := utf8.DecodeRuneInString(s[i:]) - if enc.tryAddRuneError(r, size) { - i++ - continue - } - enc.buf.AppendString(s[i : i+size]) - i += size - } -} - -// safeAddByteString is no-alloc equivalent of safeAddString(string(s)) for s []byte. -func (enc *jsonEncoder) safeAddByteString(s []byte) { - for i := 0; i < len(s); { - if enc.tryAddRuneSelf(s[i]) { - i++ - continue - } - r, size := utf8.DecodeRune(s[i:]) - if enc.tryAddRuneError(r, size) { - i++ - continue - } - enc.buf.Write(s[i : i+size]) - i += size - } -} - -// tryAddRuneSelf appends b if it is valid UTF-8 character represented in a single byte. -func (enc *jsonEncoder) tryAddRuneSelf(b byte) bool { - if b >= utf8.RuneSelf { - return false - } - if 0x20 <= b && b != '\\' && b != '"' { - enc.buf.AppendByte(b) - return true - } - switch b { - case '\\', '"': - enc.buf.AppendByte('\\') - enc.buf.AppendByte(b) - case '\n': - enc.buf.AppendByte('\\') - enc.buf.AppendByte('n') - case '\r': - enc.buf.AppendByte('\\') - enc.buf.AppendByte('r') - case '\t': - enc.buf.AppendByte('\\') - enc.buf.AppendByte('t') - default: - // Encode bytes < 0x20, except for the escape sequences above. - enc.buf.AppendString(`\u00`) - enc.buf.AppendByte(_hex[b>>4]) - enc.buf.AppendByte(_hex[b&0xF]) - } - return true -} - -func (enc *jsonEncoder) tryAddRuneError(r rune, size int) bool { - if r == utf8.RuneError && size == 1 { - enc.buf.AppendString(`\ufffd`) - return true - } - return false -} diff --git a/pkg/log/internal/core/pool.go b/pkg/log/internal/core/pool.go deleted file mode 100644 index 6644d5067..000000000 --- a/pkg/log/internal/core/pool.go +++ /dev/null @@ -1,52 +0,0 @@ -// Copyright (c) 2016 Uber Technologies, Inc. -// -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the "Software"), to deal -// in the Software without restriction, including without limitation the rights -// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -// copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: -// -// The above copyright notice and this permission notice shall be included in -// all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -// THE SOFTWARE. - -package core - -import "sync" - -// A Pool is a type-safe wrapper around a sync.Pool. -type Pool struct { - p *sync.Pool -} - -// NewPool constructs a new Pool. -func NewPool(size int) Pool { - if size == 0 { - size = _size - } - return Pool{p: &sync.Pool{ - New: func() interface{} { - return &Buffer{bs: make([]byte, 0, size)} - }, - }} -} - -// Get retrieves a Buffer from the pool, creating one if necessary. -func (p Pool) Get() *Buffer { - buf := p.p.Get().(*Buffer) - buf.Reset() - buf.pool = p - return buf -} - -func (p Pool) put(buf *Buffer) { - p.p.Put(buf) -} diff --git a/pkg/log/internal/core/pool_test.go b/pkg/log/internal/core/pool_test.go deleted file mode 100644 index 33bb4ff52..000000000 --- a/pkg/log/internal/core/pool_test.go +++ /dev/null @@ -1,52 +0,0 @@ -// Copyright (c) 2016 Uber Technologies, Inc. -// -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the "Software"), to deal -// in the Software without restriction, including without limitation the rights -// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -// copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: -// -// The above copyright notice and this permission notice shall be included in -// all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -// THE SOFTWARE. - -package core - -import ( - "sync" - "testing" - - "github.com/stretchr/testify/assert" -) - -func TestBuffers(t *testing.T) { - const dummyData = "dummy data" - p := NewPool(0) - - var wg sync.WaitGroup - for g := 0; g < 10; g++ { - wg.Add(1) - go func() { - for i := 0; i < 100; i++ { - buf := p.Get() - assert.Zero(t, buf.Len(), "Expected truncated buffer") - assert.NotZero(t, buf.Cap(), "Expected non-zero capacity") - - buf.AppendString(dummyData) - assert.Equal(t, buf.Len(), len(dummyData), "Expected buffer to contain dummy data") - - buf.Free() - } - wg.Done() - }() - } - wg.Wait() -} diff --git a/pkg/log/internal/filewriter/filewriter.go b/pkg/log/internal/filewriter/filewriter.go deleted file mode 100644 index f981cc9fe..000000000 --- a/pkg/log/internal/filewriter/filewriter.go +++ /dev/null @@ -1,344 +0,0 @@ -package filewriter - -import ( - "bytes" - "container/list" - "fmt" - "io/ioutil" - "log" - "os" - "path/filepath" - "sort" - "strconv" - "strings" - "sync" - "sync/atomic" - "time" -) - -// FileWriter create file log writer -type FileWriter struct { - opt option - dir string - fname string - ch chan *bytes.Buffer - stdlog *log.Logger - pool *sync.Pool - - lastRotateFormat string - lastSplitNum int - - current *wrapFile - files *list.List - - closed int32 - wg sync.WaitGroup -} - -type rotateItem struct { - rotateTime int64 - rotateNum int - fname string -} - -func parseRotateItem(dir, fname, rotateFormat string) (*list.List, error) { - fis, err := ioutil.ReadDir(dir) - if err != nil { - return nil, err - } - - // parse exists log file filename - parse := func(s string) (rt rotateItem, err error) { - // remove filename and left "." error.log.2018-09-12.001 -> 2018-09-12.001 - rt.fname = s - s = strings.TrimLeft(s[len(fname):], ".") - seqs := strings.Split(s, ".") - var t time.Time - switch len(seqs) { - case 2: - if rt.rotateNum, err = strconv.Atoi(seqs[1]); err != nil { - return - } - fallthrough - case 1: - if t, err = time.Parse(rotateFormat, seqs[0]); err != nil { - return - } - rt.rotateTime = t.Unix() - } - return - } - - var items []rotateItem - for _, fi := range fis { - if strings.HasPrefix(fi.Name(), fname) && fi.Name() != fname { - rt, err := parse(fi.Name()) - if err != nil { - // TODO deal with error - continue - } - items = append(items, rt) - } - } - sort.Slice(items, func(i, j int) bool { - if items[i].rotateTime == items[j].rotateTime { - return items[i].rotateNum > items[j].rotateNum - } - return items[i].rotateTime > items[j].rotateTime - }) - l := list.New() - - for _, item := range items { - l.PushBack(item) - } - return l, nil -} - -type wrapFile struct { - fsize int64 - fp *os.File -} - -func (w *wrapFile) size() int64 { - return w.fsize -} - -func (w *wrapFile) write(p []byte) (n int, err error) { - n, err = w.fp.Write(p) - w.fsize += int64(n) - return -} - -func newWrapFile(fpath string) (*wrapFile, error) { - fp, err := os.OpenFile(fpath, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644) - if err != nil { - return nil, err - } - fi, err := fp.Stat() - if err != nil { - return nil, err - } - return &wrapFile{fp: fp, fsize: fi.Size()}, nil -} - -// New FileWriter A FileWriter is safe for use by multiple goroutines simultaneously. -func New(fpath string, fns ...Option) (*FileWriter, error) { - opt := defaultOption - for _, fn := range fns { - fn(&opt) - } - - fname := filepath.Base(fpath) - if fname == "" { - return nil, fmt.Errorf("filename can't empty") - } - dir := filepath.Dir(fpath) - fi, err := os.Stat(dir) - if err == nil && !fi.IsDir() { - return nil, fmt.Errorf("%s already exists and not a directory", dir) - } - if os.IsNotExist(err) { - if err = os.MkdirAll(dir, 0755); err != nil { - return nil, fmt.Errorf("create dir %s error: %s", dir, err.Error()) - } - } - - current, err := newWrapFile(fpath) - if err != nil { - return nil, err - } - - stdlog := log.New(os.Stderr, "flog ", log.LstdFlags) - ch := make(chan *bytes.Buffer, opt.ChanSize) - - files, err := parseRotateItem(dir, fname, opt.RotateFormat) - if err != nil { - // set files a empty list - files = list.New() - stdlog.Printf("parseRotateItem error: %s", err) - } - - lastRotateFormat := time.Now().Format(opt.RotateFormat) - var lastSplitNum int - if files.Len() > 0 { - rt := files.Front().Value.(rotateItem) - // check contains is mush esay than compared with timestamp - if strings.Contains(rt.fname, lastRotateFormat) { - lastSplitNum = rt.rotateNum - } - } - - fw := &FileWriter{ - opt: opt, - dir: dir, - fname: fname, - stdlog: stdlog, - ch: ch, - pool: &sync.Pool{New: func() interface{} { return new(bytes.Buffer) }}, - - lastSplitNum: lastSplitNum, - lastRotateFormat: lastRotateFormat, - - files: files, - current: current, - } - - fw.wg.Add(1) - go fw.daemon() - - return fw, nil -} - -// Write write data to log file, return write bytes is pseudo just for implement io.Writer. -func (f *FileWriter) Write(p []byte) (int, error) { - // atomic is not necessary - if atomic.LoadInt32(&f.closed) == 1 { - f.stdlog.Printf("%s", p) - return 0, fmt.Errorf("filewriter already closed") - } - // because write to file is asynchronousc, - // copy p to internal buf prevent p be change on outside - buf := f.getBuf() - buf.Write(p) - - if f.opt.WriteTimeout == 0 { - select { - case f.ch <- buf: - return len(p), nil - default: - // TODO: write discard log to to stdout? - return 0, fmt.Errorf("log channel is full, discard log") - } - } - - // write log with timeout - timeout := time.NewTimer(f.opt.WriteTimeout) - select { - case f.ch <- buf: - return len(p), nil - case <-timeout.C: - // TODO: write discard log to to stdout? - return 0, fmt.Errorf("log channel is full, discard log") - } -} - -func (f *FileWriter) daemon() { - // TODO: check aggsbuf size prevent it too big - aggsbuf := &bytes.Buffer{} - tk := time.NewTicker(f.opt.RotateInterval) - // TODO: make it configrable - aggstk := time.NewTicker(10 * time.Millisecond) - var err error - for { - select { - case t := <-tk.C: - f.checkRotate(t) - case buf, ok := <-f.ch: - if ok { - aggsbuf.Write(buf.Bytes()) - f.putBuf(buf) - } - case <-aggstk.C: - if aggsbuf.Len() > 0 { - if err = f.write(aggsbuf.Bytes()); err != nil { - f.stdlog.Printf("write log error: %s", err) - } - aggsbuf.Reset() - } - } - if atomic.LoadInt32(&f.closed) != 1 { - continue - } - // read all buf from channel and break loop - if err = f.write(aggsbuf.Bytes()); err != nil { - f.stdlog.Printf("write log error: %s", err) - } - for buf := range f.ch { - if err = f.write(buf.Bytes()); err != nil { - f.stdlog.Printf("write log error: %s", err) - } - f.putBuf(buf) - } - break - } - f.wg.Done() -} - -// Close close file writer -func (f *FileWriter) Close() error { - atomic.StoreInt32(&f.closed, 1) - close(f.ch) - f.wg.Wait() - return nil -} - -func (f *FileWriter) checkRotate(t time.Time) { - formatFname := func(format string, num int) string { - if num == 0 { - return fmt.Sprintf("%s.%s", f.fname, format) - } - return fmt.Sprintf("%s.%s.%03d", f.fname, format, num) - } - format := t.Format(f.opt.RotateFormat) - - if f.opt.MaxFile != 0 { - for f.files.Len() > f.opt.MaxFile { - rt := f.files.Remove(f.files.Front()).(rotateItem) - fpath := filepath.Join(f.dir, rt.fname) - if err := os.Remove(fpath); err != nil { - f.stdlog.Printf("remove file %s error: %s", fpath, err) - } - } - } - - if format != f.lastRotateFormat || (f.opt.MaxSize != 0 && f.current.size() > f.opt.MaxSize) { - var err error - // close current file first - if err = f.current.fp.Close(); err != nil { - f.stdlog.Printf("close current file error: %s", err) - } - - // rename file - fname := formatFname(f.lastRotateFormat, f.lastSplitNum) - oldpath := filepath.Join(f.dir, f.fname) - newpath := filepath.Join(f.dir, fname) - if err = os.Rename(oldpath, newpath); err != nil { - f.stdlog.Printf("rename file %s to %s error: %s", oldpath, newpath, err) - return - } - - f.files.PushBack(rotateItem{fname: fname /*rotateNum: f.lastSplitNum, rotateTime: t.Unix() unnecessary*/}) - - if format != f.lastRotateFormat { - f.lastRotateFormat = format - f.lastSplitNum = 0 - } else { - f.lastSplitNum++ - } - - // recreate current file - f.current, err = newWrapFile(filepath.Join(f.dir, f.fname)) - if err != nil { - f.stdlog.Printf("create log file error: %s", err) - } - } -} - -func (f *FileWriter) write(p []byte) error { - // f.current may be nil, if newWrapFile return err in checkRotate, redirect log to stderr - if f.current == nil { - f.stdlog.Printf("can't write log to file, please check stderr log for detail") - f.stdlog.Printf("%s", p) - } - _, err := f.current.write(p) - return err -} - -func (f *FileWriter) putBuf(buf *bytes.Buffer) { - buf.Reset() - f.pool.Put(buf) -} - -func (f *FileWriter) getBuf() *bytes.Buffer { - return f.pool.Get().(*bytes.Buffer) -} diff --git a/pkg/log/internal/filewriter/filewriter_test.go b/pkg/log/internal/filewriter/filewriter_test.go deleted file mode 100644 index 6178da78b..000000000 --- a/pkg/log/internal/filewriter/filewriter_test.go +++ /dev/null @@ -1,221 +0,0 @@ -package filewriter - -import ( - "fmt" - "io/ioutil" - "os" - "path/filepath" - "testing" - "time" - - "github.com/stretchr/testify/assert" -) - -const logdir = "testlog" - -func touch(dir, name string) { - os.MkdirAll(dir, 0755) - fp, err := os.OpenFile(filepath.Join(dir, name), os.O_CREATE, 0644) - if err != nil { - panic(err) - } - fp.Close() -} - -func TestMain(m *testing.M) { - ret := m.Run() - os.RemoveAll(logdir) - os.Exit(ret) -} - -func TestParseRotate(t *testing.T) { - touch := func(dir, name string) { - os.MkdirAll(dir, 0755) - fp, err := os.OpenFile(filepath.Join(dir, name), os.O_CREATE, 0644) - if err != nil { - t.Fatal(err) - } - fp.Close() - } - dir := filepath.Join(logdir, "test-parse-rotate") - names := []string{"info.log.2018-11-11", "info.log.2018-11-11.001", "info.log.2018-11-11.002", "info.log." + time.Now().Format("2006-01-02") + ".005"} - for _, name := range names { - touch(dir, name) - } - l, err := parseRotateItem(dir, "info.log", "2006-01-02") - if err != nil { - t.Fatal(err) - } - - assert.Equal(t, len(names), l.Len()) - - rt := l.Front().Value.(rotateItem) - - assert.Equal(t, 5, rt.rotateNum) -} - -func TestRotateExists(t *testing.T) { - dir := filepath.Join(logdir, "test-rotate-exists") - names := []string{"info.log." + time.Now().Format("2006-01-02") + ".005"} - for _, name := range names { - touch(dir, name) - } - fw, err := New(logdir+"/test-rotate-exists/info.log", - MaxSize(1024*1024), - func(opt *option) { opt.RotateInterval = time.Millisecond }, - ) - if err != nil { - t.Fatal(err) - } - data := make([]byte, 1024) - for i := range data { - data[i] = byte(i) - } - for i := 0; i < 10; i++ { - for i := 0; i < 1024; i++ { - _, err = fw.Write(data) - if err != nil { - t.Error(err) - } - } - time.Sleep(10 * time.Millisecond) - } - fw.Close() - fis, err := ioutil.ReadDir(logdir + "/test-rotate-exists") - if err != nil { - t.Fatal(err) - } - var fnams []string - for _, fi := range fis { - fnams = append(fnams, fi.Name()) - } - assert.Contains(t, fnams, "info.log."+time.Now().Format("2006-01-02")+".006") -} - -func TestSizeRotate(t *testing.T) { - fw, err := New(logdir+"/test-rotate/info.log", - MaxSize(1024*1024), - func(opt *option) { opt.RotateInterval = 1 * time.Millisecond }, - ) - if err != nil { - t.Fatal(err) - } - data := make([]byte, 1024) - for i := range data { - data[i] = byte(i) - } - for i := 0; i < 10; i++ { - for i := 0; i < 1024; i++ { - _, err = fw.Write(data) - if err != nil { - t.Error(err) - } - } - time.Sleep(10 * time.Millisecond) - } - fw.Close() - fis, err := ioutil.ReadDir(logdir + "/test-rotate") - if err != nil { - t.Fatal(err) - } - assert.True(t, len(fis) > 5, "expect more than 5 file get %d", len(fis)) -} - -func TestMaxFile(t *testing.T) { - fw, err := New(logdir+"/test-maxfile/info.log", - MaxSize(1024*1024), - MaxFile(1), - func(opt *option) { opt.RotateInterval = 1 * time.Millisecond }, - ) - if err != nil { - t.Fatal(err) - } - data := make([]byte, 1024) - for i := range data { - data[i] = byte(i) - } - for i := 0; i < 10; i++ { - for i := 0; i < 1024; i++ { - _, err = fw.Write(data) - if err != nil { - t.Error(err) - } - } - time.Sleep(10 * time.Millisecond) - } - fw.Close() - fis, err := ioutil.ReadDir(logdir + "/test-maxfile") - if err != nil { - t.Fatal(err) - } - assert.True(t, len(fis) <= 2, fmt.Sprintf("expect 2 file get %d", len(fis))) -} - -func TestMaxFile2(t *testing.T) { - files := []string{ - "info.log.2018-12-01", - "info.log.2018-12-02", - "info.log.2018-12-03", - "info.log.2018-12-04", - "info.log.2018-12-05", - "info.log.2018-12-05.001", - } - for _, file := range files { - touch(logdir+"/test-maxfile2", file) - } - fw, err := New(logdir+"/test-maxfile2/info.log", - MaxSize(1024*1024), - MaxFile(3), - func(opt *option) { opt.RotateInterval = 1 * time.Millisecond }, - ) - if err != nil { - t.Fatal(err) - } - data := make([]byte, 1024) - for i := range data { - data[i] = byte(i) - } - for i := 0; i < 10; i++ { - for i := 0; i < 1024; i++ { - _, err = fw.Write(data) - if err != nil { - t.Error(err) - } - } - time.Sleep(10 * time.Millisecond) - } - fw.Close() - fis, err := ioutil.ReadDir(logdir + "/test-maxfile2") - if err != nil { - t.Fatal(err) - } - assert.True(t, len(fis) == 4, fmt.Sprintf("expect 4 file get %d", len(fis))) -} - -func TestFileWriter(t *testing.T) { - fw, err := New("testlog/info.log") - if err != nil { - t.Fatal(err) - } - defer fw.Close() - _, err = fw.Write([]byte("Hello World!\n")) - if err != nil { - t.Error(err) - } -} - -func BenchmarkFileWriter(b *testing.B) { - fw, err := New("testlog/bench/info.log", - func(opt *option) { opt.WriteTimeout = time.Second }, MaxSize(1024*1024*8), /*32MB*/ - func(opt *option) { opt.RotateInterval = 10 * time.Millisecond }, - ) - if err != nil { - b.Fatal(err) - } - for i := 0; i < b.N; i++ { - _, err = fw.Write([]byte("Hello World!\n")) - if err != nil { - b.Error(err) - } - } -} diff --git a/pkg/log/internal/filewriter/option.go b/pkg/log/internal/filewriter/option.go deleted file mode 100644 index dbe19d671..000000000 --- a/pkg/log/internal/filewriter/option.go +++ /dev/null @@ -1,69 +0,0 @@ -package filewriter - -import ( - "fmt" - "strings" - "time" -) - -// RotateFormat -const ( - RotateDaily = "2006-01-02" -) - -var defaultOption = option{ - RotateFormat: RotateDaily, - MaxSize: 1 << 30, - ChanSize: 1024 * 8, - RotateInterval: 10 * time.Second, -} - -type option struct { - RotateFormat string - MaxFile int - MaxSize int64 - ChanSize int - - // TODO export Option - RotateInterval time.Duration - WriteTimeout time.Duration -} - -// Option filewriter option -type Option func(opt *option) - -// RotateFormat e.g 2006-01-02 meaning rotate log file every day. -// NOTE: format can't contain ".", "." will cause panic ヽ(*。>Д<)o゜. -func RotateFormat(format string) Option { - if strings.Contains(format, ".") { - panic(fmt.Sprintf("rotate format can't contain '.' format: %s", format)) - } - return func(opt *option) { - opt.RotateFormat = format - } -} - -// MaxFile default 999, 0 meaning unlimit. -// TODO: don't create file list if MaxSize is unlimt. -func MaxFile(n int) Option { - return func(opt *option) { - opt.MaxFile = n - } -} - -// MaxSize set max size for single log file, -// defult 1GB, 0 meaning unlimit. -func MaxSize(n int64) Option { - return func(opt *option) { - opt.MaxSize = n - } -} - -// ChanSize set internal chan size default 8192 use about 64k memory on x64 platform static, -// because filewriter has internal object pool, change chan size bigger may cause filewriter use -// a lot of memory, because sync.Pool can't set expire time memory won't free until program exit. -func ChanSize(n int) Option { - return func(opt *option) { - opt.ChanSize = n - } -} diff --git a/pkg/log/level.go b/pkg/log/level.go deleted file mode 100644 index fa4c98eaf..000000000 --- a/pkg/log/level.go +++ /dev/null @@ -1,29 +0,0 @@ -package log - -// Level of severity. -type Level int - -// Verbose is a boolean type that implements Info, Infov (like Printf) etc. -type Verbose bool - -// common log level. -const ( - _debugLevel Level = iota - _infoLevel - _warnLevel - _errorLevel - _fatalLevel -) - -var levelNames = [...]string{ - _debugLevel: "DEBUG", - _infoLevel: "INFO", - _warnLevel: "WARN", - _errorLevel: "ERROR", - _fatalLevel: "FATAL", -} - -// String implementation. -func (l Level) String() string { - return levelNames[l] -} diff --git a/pkg/log/log.go b/pkg/log/log.go deleted file mode 100644 index 9d5c30f79..000000000 --- a/pkg/log/log.go +++ /dev/null @@ -1,325 +0,0 @@ -package log - -import ( - "context" - "flag" - "fmt" - "io" - "os" - "strconv" - - "github.com/go-kratos/kratos/pkg/conf/env" - "github.com/go-kratos/kratos/pkg/stat/metric" -) - -// Config log config. -type Config struct { - Family string - Host string - - // stdout - Stdout bool - - // file - Dir string - // buffer size - FileBufferSize int64 - // MaxLogFile - MaxLogFile int - // RotateSize - RotateSize int64 - - // V Enable V-leveled logging at the specified level. - V int32 - // Module="" - // The syntax of the argument is a map of pattern=N, - // where pattern is a literal file name (minus the ".go" suffix) or - // "glob" pattern and N is a V level. For instance: - // [module] - // "service" = 1 - // "dao*" = 2 - // sets the V level to 2 in all Go files whose names begin "dao". - Module map[string]int32 - // Filter tell log handler which field are sensitive message, use * instead. - Filter []string -} - -// metricErrCount prometheus error counter. -var ( - metricErrCount = metric.NewBusinessMetricCount("log_error_total", "source") -) - -// Render render log output -type Render interface { - Render(io.Writer, map[string]interface{}) error - RenderString(map[string]interface{}) string -} - -var ( - h Handler - c *Config -) - -func init() { - host, _ := os.Hostname() - c = &Config{ - Family: env.AppID, - Host: host, - } - h = newHandlers([]string{}, NewStdout()) - - addFlag(flag.CommandLine) -} - -var ( - _v int - _stdout bool - _dir string - _agentDSN string - _filter logFilter - _module = verboseModule{} - _noagent bool -) - -// addFlag init log from dsn. -func addFlag(fs *flag.FlagSet) { - if lv, err := strconv.ParseInt(os.Getenv("LOG_V"), 10, 64); err == nil { - _v = int(lv) - } - _stdout, _ = strconv.ParseBool(os.Getenv("LOG_STDOUT")) - _dir = os.Getenv("LOG_DIR") - if tm := os.Getenv("LOG_MODULE"); len(tm) > 0 { - _module.Set(tm) - } - if tf := os.Getenv("LOG_FILTER"); len(tf) > 0 { - _filter.Set(tf) - } - _noagent, _ = strconv.ParseBool(os.Getenv("LOG_NO_AGENT")) - // get val from flag - fs.IntVar(&_v, "log.v", _v, "log verbose level, or use LOG_V env variable.") - fs.BoolVar(&_stdout, "log.stdout", _stdout, "log enable stdout or not, or use LOG_STDOUT env variable.") - fs.StringVar(&_dir, "log.dir", _dir, "log file `path, or use LOG_DIR env variable.") - fs.StringVar(&_agentDSN, "log.agent", _agentDSN, "log agent dsn, or use LOG_AGENT env variable.") - fs.Var(&_module, "log.module", "log verbose for specified module, or use LOG_MODULE env variable, format: file=1,file2=2.") - fs.Var(&_filter, "log.filter", "log field for sensitive message, or use LOG_FILTER env variable, format: field1,field2.") - fs.BoolVar(&_noagent, "log.noagent", _noagent, "force disable log agent print log to stderr, or use LOG_NO_AGENT") -} - -// Init create logger with context. -func Init(conf *Config) { - var isNil bool - if conf == nil { - isNil = true - conf = &Config{ - Stdout: _stdout, - Dir: _dir, - V: int32(_v), - Module: _module, - Filter: _filter, - } - } - if len(env.AppID) != 0 { - conf.Family = env.AppID // for caster - } - conf.Host = env.Hostname - if len(conf.Host) == 0 { - host, _ := os.Hostname() - conf.Host = host - } - var hs []Handler - // when env is dev - if conf.Stdout || (isNil && (env.DeployEnv == "" || env.DeployEnv == env.DeployEnvDev)) || _noagent { - hs = append(hs, NewStdout()) - } - if conf.Dir != "" { - hs = append(hs, NewFile(conf.Dir, conf.FileBufferSize, conf.RotateSize, conf.MaxLogFile)) - } - h = newHandlers(conf.Filter, hs...) - c = conf -} - -// Debug logs a message at the debug log level. -func Debug(format string, args ...interface{}) { - if int32(_debugLevel) >= c.V { - h.Log(context.Background(), _debugLevel, KVString(_log, fmt.Sprintf(format, args...))) - } -} - -// Info logs a message at the info log level. -func Info(format string, args ...interface{}) { - if int32(_infoLevel) >= c.V { - h.Log(context.Background(), _infoLevel, KVString(_log, fmt.Sprintf(format, args...))) - } -} - -// Warn logs a message at the warning log level. -func Warn(format string, args ...interface{}) { - if int32(_warnLevel) >= c.V { - h.Log(context.Background(), _warnLevel, KVString(_log, fmt.Sprintf(format, args...))) - } -} - -// Error logs a message at the error log level. -func Error(format string, args ...interface{}) { - if int32(_errorLevel) >= c.V { - h.Log(context.Background(), _errorLevel, KVString(_log, fmt.Sprintf(format, args...))) - } -} - -// Fatal logs a message at the fatal log level. -func Fatal(format string, args ...interface{}) { - if int32(_fatalLevel) >= c.V { - h.Log(context.Background(), _fatalLevel, KVString(_log, fmt.Sprintf(format, args...))) - } -} - -// Debugc logs a message at the debug log level. -func Debugc(ctx context.Context, format string, args ...interface{}) { - if int32(_debugLevel) >= c.V { - h.Log(ctx, _debugLevel, KVString(_log, fmt.Sprintf(format, args...))) - } -} - -// Infoc logs a message at the info log level. -func Infoc(ctx context.Context, format string, args ...interface{}) { - if int32(_infoLevel) >= c.V { - h.Log(ctx, _infoLevel, KVString(_log, fmt.Sprintf(format, args...))) - } -} - -// Errorc logs a message at the error log level. -func Errorc(ctx context.Context, format string, args ...interface{}) { - if int32(_errorLevel) >= c.V { - h.Log(ctx, _errorLevel, KVString(_log, fmt.Sprintf(format, args...))) - } -} - -// Warnc logs a message at the warning log level. -func Warnc(ctx context.Context, format string, args ...interface{}) { - if int32(_warnLevel) >= c.V { - h.Log(ctx, _warnLevel, KVString(_log, fmt.Sprintf(format, args...))) - } -} - -// Fatalc logs a message at the fatal log level. -func Fatalc(ctx context.Context, format string, args ...interface{}) { - if int32(_fatalLevel) >= c.V { - h.Log(ctx, _fatalLevel, KVString(_log, fmt.Sprintf(format, args...))) - } -} - -// Debugv logs a message at the debug log level. -func Debugv(ctx context.Context, args ...D) { - if int32(_debugLevel) >= c.V { - h.Log(ctx, _debugLevel, args...) - } -} - -// Infov logs a message at the info log level. -func Infov(ctx context.Context, args ...D) { - if int32(_infoLevel) >= c.V { - h.Log(ctx, _infoLevel, args...) - } -} - -// Warnv logs a message at the warning log level. -func Warnv(ctx context.Context, args ...D) { - if int32(_warnLevel) >= c.V { - h.Log(ctx, _warnLevel, args...) - } -} - -// Errorv logs a message at the error log level. -func Errorv(ctx context.Context, args ...D) { - if int32(_errorLevel) >= c.V { - h.Log(ctx, _errorLevel, args...) - } -} - -// Fatalv logs a message at the error log level. -func Fatalv(ctx context.Context, args ...D) { - if int32(_fatalLevel) >= c.V { - h.Log(ctx, _fatalLevel, args...) - } -} - -func logw(args []interface{}) []D { - if len(args)%2 != 0 { - Warn("log: the variadic must be plural, the last one will ignored") - } - ds := make([]D, 0, len(args)/2) - for i := 0; i < len(args)-1; i = i + 2 { - if key, ok := args[i].(string); ok { - ds = append(ds, KV(key, args[i+1])) - } else { - Warn("log: key must be string, get %T, ignored", args[i]) - } - } - return ds -} - -// Debugw logs a message with some additional context. The variadic key-value pairs are treated as they are in With. -func Debugw(ctx context.Context, args ...interface{}) { - if int32(_debugLevel) >= c.V { - h.Log(ctx, _debugLevel, logw(args)...) - } -} - -// Infow logs a message with some additional context. The variadic key-value pairs are treated as they are in With. -func Infow(ctx context.Context, args ...interface{}) { - if int32(_infoLevel) >= c.V { - h.Log(ctx, _infoLevel, logw(args)...) - } -} - -// Warnw logs a message with some additional context. The variadic key-value pairs are treated as they are in With. -func Warnw(ctx context.Context, args ...interface{}) { - if int32(_warnLevel) >= c.V { - h.Log(ctx, _warnLevel, logw(args)...) - } -} - -// Errorw logs a message with some additional context. The variadic key-value pairs are treated as they are in With. -func Errorw(ctx context.Context, args ...interface{}) { - if int32(_errorLevel) >= c.V { - h.Log(ctx, _errorLevel, logw(args)...) - } -} - -// Fatalw logs a message with some additional context. The variadic key-value pairs are treated as they are in With. -func Fatalw(ctx context.Context, args ...interface{}) { - if int32(_fatalLevel) >= c.V { - h.Log(ctx, _fatalLevel, logw(args)...) - } -} - -// SetFormat only effective on stdout and file handler -// %T time format at "15:04:05.999" on stdout handler, "15:04:05 MST" on file handler -// %t time format at "15:04:05" on stdout handler, "15:04" on file on file handler -// %D data format at "2006/01/02" -// %d data format at "01/02" -// %L log level e.g. INFO WARN ERROR -// %M log message and additional fields: key=value this is log message -// NOTE below pattern not support on file handler -// %f function name and line number e.g. model.Get:121 -// %i instance id -// %e deploy env e.g. dev uat fat prod -// %z zone -// %S full file name and line number: /a/b/c/d.go:23 -// %s final file name element and line number: d.go:23 -func SetFormat(format string) { - h.SetFormat(format) -} - -// Close close resource. -func Close() (err error) { - err = h.Close() - h = _defaultStdout - return -} - -func errIncr(lv Level, source string) { - if lv == _errorLevel { - metricErrCount.Inc(source) - } -} diff --git a/pkg/log/log_test.go b/pkg/log/log_test.go deleted file mode 100644 index 65f503853..000000000 --- a/pkg/log/log_test.go +++ /dev/null @@ -1,114 +0,0 @@ -package log - -import ( - "context" - "testing" - - "github.com/go-kratos/kratos/pkg/net/metadata" - - "github.com/stretchr/testify/assert" -) - -func initStdout() { - conf := &Config{ - Stdout: true, - } - Init(conf) -} - -func initFile() { - conf := &Config{ - Dir: "/tmp", - // VLevel: 2, - Module: map[string]int32{"log_test": 1}, - } - Init(conf) -} - -type TestLog struct { - A string - B int - C string - D string -} - -func testLog(t *testing.T) { - t.Run("Fatal", func(t *testing.T) { - Fatal("hello %s", "world") - Fatalv(context.Background(), KV("key", 2222222), KV("test2", "test")) - Fatalc(context.Background(), "keys: %s %s...", "key1", "key2") - }) - t.Run("Error", func(t *testing.T) { - Error("hello %s", "world") - Errorv(context.Background(), KV("key", 2222222), KV("test2", "test")) - Errorc(context.Background(), "keys: %s %s...", "key1", "key2") - }) - t.Run("Warn", func(t *testing.T) { - Warn("hello %s", "world") - Warnv(context.Background(), KV("key", 2222222), KV("test2", "test")) - Warnc(context.Background(), "keys: %s %s...", "key1", "key2") - }) - t.Run("Info", func(t *testing.T) { - Info("hello %s", "world") - Infov(context.Background(), KV("key", 2222222), KV("test2", "test")) - Infoc(context.Background(), "keys: %s %s...", "key1", "key2") - }) - t.Run("Debug", func(t *testing.T) { - Debug("hello %s", "world") - Debugv(context.Background(), KV("key", 2222222), KV("test2", "test")) - Debugc(context.Background(), "keys: %s %s...", "key1", "key2") - }) -} - -func TestFile(t *testing.T) { - initFile() - testLog(t) - assert.Equal(t, nil, Close()) -} - -func TestStdout(t *testing.T) { - initStdout() - testLog(t) - assert.Equal(t, nil, Close()) -} - -func TestLogW(t *testing.T) { - D := logw([]interface{}{"i", "like", "a", "dog"}) - if len(D) != 2 || D[0].Key != "i" || D[0].Value != "like" || D[1].Key != "a" || D[1].Value != "dog" { - t.Fatalf("logw out put should be ' {i like} {a dog}'") - } - D = logw([]interface{}{"i", "like", "dog"}) - if len(D) != 1 || D[0].Key != "i" || D[0].Value != "like" { - t.Fatalf("logw out put should be ' {i like}'") - } -} - -func TestLogWithMirror(t *testing.T) { - Info("test log") - mdcontext := metadata.NewContext(context.Background(), metadata.MD{metadata.Mirror: "true"}) - Infov(mdcontext, KV("key1", "val1"), KV("key2", ""), KV("log", "log content"), KV("msg", "msg content")) - - mdcontext = metadata.NewContext(context.Background(), metadata.MD{metadata.Mirror: "***"}) - Infov(mdcontext, KV("key1", "val1"), KV("key2", ""), KV("log", "log content"), KV("msg", "msg content")) - - Infov(context.Background(), KV("key1", "val1"), KV("key2", ""), KV("log", "log content"), KV("msg", "msg content")) -} - -func TestOverwriteSouce(t *testing.T) { - ctx := context.Background() - t.Run("test source kv string", func(t *testing.T) { - Infov(ctx, KVString("source", "test")) - }) - t.Run("test source kv string", func(t *testing.T) { - Infov(ctx, KV("source", "test")) - }) -} - -func BenchmarkLog(b *testing.B) { - ctx := context.Background() - b.RunParallel(func(pb *testing.PB) { - for pb.Next() { - Infov(ctx, KVString("test", "hello"), KV("int", 34), KV("hhh", "hhhh")) - } - }) -} diff --git a/pkg/log/logrus.go b/pkg/log/logrus.go deleted file mode 100644 index 96523077f..000000000 --- a/pkg/log/logrus.go +++ /dev/null @@ -1,61 +0,0 @@ -package log - -import ( - "context" - "io/ioutil" - "os" - - "github.com/sirupsen/logrus" -) - -func init() { - redirectLogrus() -} - -func redirectLogrus() { - // FIXME: because of different stack depth call runtime.Caller will get error function name. - logrus.AddHook(redirectHook{}) - if os.Getenv("LOGRUS_STDOUT") == "" { - logrus.SetOutput(ioutil.Discard) - } -} - -type redirectHook struct{} - -func (redirectHook) Levels() []logrus.Level { - return logrus.AllLevels -} - -func (redirectHook) Fire(entry *logrus.Entry) error { - lv := _infoLevel - var logrusLv string - var verbose int32 - switch entry.Level { - case logrus.FatalLevel, logrus.PanicLevel: - logrusLv = entry.Level.String() - fallthrough - case logrus.ErrorLevel: - lv = _errorLevel - case logrus.WarnLevel: - lv = _warnLevel - case logrus.InfoLevel: - lv = _infoLevel - case logrus.DebugLevel: - // use verbose log replace of debuglevel - verbose = 10 - } - args := make([]D, 0, len(entry.Data)+1) - args = append(args, D{Key: _log, Value: entry.Message}) - for k, v := range entry.Data { - args = append(args, D{Key: k, Value: v}) - } - if logrusLv != "" { - args = append(args, D{Key: "logrus_lv", Value: logrusLv}) - } - if verbose != 0 { - V(verbose).Infov(context.Background(), args...) - } else { - h.Log(context.Background(), lv, args...) - } - return nil -} diff --git a/pkg/log/pattern.go b/pkg/log/pattern.go deleted file mode 100644 index 3863e6052..000000000 --- a/pkg/log/pattern.go +++ /dev/null @@ -1,164 +0,0 @@ -package log - -import ( - "fmt" - "io" - "path" - "strings" - "sync" - "time" -) - -var patternMap = map[string]func(map[string]interface{}) string{ - "T": longTime, - "t": shortTime, - "D": longDate, - "d": shortDate, - "L": keyFactory(_level), - "f": keyFactory(_source), - "i": keyFactory(_instanceID), - "e": keyFactory(_deplyEnv), - "z": keyFactory(_zone), - "S": longSource, - "s": shortSource, - "M": message, -} - -// newPatternRender new pattern render -func newPatternRender(format string) Render { - p := &pattern{ - bufPool: sync.Pool{New: func() interface{} { return &strings.Builder{} }}, - } - b := make([]byte, 0, len(format)) - for i := 0; i < len(format); i++ { - if format[i] != '%' { - b = append(b, format[i]) - continue - } - if i+1 >= len(format) { - b = append(b, format[i]) - continue - } - f, ok := patternMap[string(format[i+1])] - if !ok { - b = append(b, format[i]) - continue - } - if len(b) != 0 { - p.funcs = append(p.funcs, textFactory(string(b))) - b = b[:0] - } - p.funcs = append(p.funcs, f) - i++ - } - if len(b) != 0 { - p.funcs = append(p.funcs, textFactory(string(b))) - } - return p -} - -type pattern struct { - funcs []func(map[string]interface{}) string - bufPool sync.Pool -} - -// Render implements Formater -func (p *pattern) Render(w io.Writer, d map[string]interface{}) error { - builder := p.bufPool.Get().(*strings.Builder) - defer func() { - builder.Reset() - p.bufPool.Put(builder) - }() - for _, f := range p.funcs { - builder.WriteString(f(d)) - } - - _, err := w.Write([]byte(builder.String())) - return err -} - -// Render implements Formater as string -func (p *pattern) RenderString(d map[string]interface{}) string { - builder := p.bufPool.Get().(*strings.Builder) - defer func() { - builder.Reset() - p.bufPool.Put(builder) - }() - for _, f := range p.funcs { - builder.WriteString(f(d)) - } - - return builder.String() -} - -func textFactory(text string) func(map[string]interface{}) string { - return func(map[string]interface{}) string { - return text - } -} -func keyFactory(key string) func(map[string]interface{}) string { - return func(d map[string]interface{}) string { - if v, ok := d[key]; ok { - if s, ok := v.(string); ok { - return s - } - return fmt.Sprint(v) - } - return "" - } -} - -func longSource(d map[string]interface{}) string { - if fn, ok := d[_source].(string); ok { - return fn - } - return "unknown:0" -} - -func shortSource(d map[string]interface{}) string { - if fn, ok := d[_source].(string); ok { - return path.Base(fn) - } - return "unknown:0" -} - -func longTime(map[string]interface{}) string { - return time.Now().Format("15:04:05.000") -} - -func shortTime(map[string]interface{}) string { - return time.Now().Format("15:04") -} - -func longDate(map[string]interface{}) string { - return time.Now().Format("2006/01/02") -} - -func shortDate(map[string]interface{}) string { - return time.Now().Format("01/02") -} - -func isInternalKey(k string) bool { - switch k { - case _level, _levelValue, _time, _source, _instanceID, _appID, _deplyEnv, _zone: - return true - } - return false -} - -func message(d map[string]interface{}) string { - var m string - var s []string - for k, v := range d { - if k == _log { - m = fmt.Sprint(v) - continue - } - if isInternalKey(k) { - continue - } - s = append(s, fmt.Sprintf("%s=%v", k, v)) - } - s = append(s, m) - return strings.Join(s, " ") -} diff --git a/pkg/log/pattern_test.go b/pkg/log/pattern_test.go deleted file mode 100644 index a6aa6be00..000000000 --- a/pkg/log/pattern_test.go +++ /dev/null @@ -1,35 +0,0 @@ -package log - -import ( - "bytes" - "strings" - "testing" - "time" - - "github.com/stretchr/testify/assert" -) - -func TestPatternDefault(t *testing.T) { - buf := &bytes.Buffer{} - p := newPatternRender("%L %T %f %M") - p.Render(buf, map[string]interface{}{_level: _infoLevel.String(), _log: "hello", _time: time.Now().Format(_timeFormat), _source: "xxx:123"}) - - fields := strings.Fields(buf.String()) - assert.Equal(t, 4, len(fields)) - assert.Equal(t, "INFO", fields[0]) - assert.Equal(t, "hello", fields[3]) -} - -func TestKV(t *testing.T) { - buf := &bytes.Buffer{} - p := newPatternRender("%M") - p.Render(buf, map[string]interface{}{_level: _infoLevel.String(), _log: "2233", "hello": "test"}) - assert.Equal(t, "hello=test 2233", buf.String()) -} - -func TestBadSymbol(t *testing.T) { - buf := &bytes.Buffer{} - p := newPatternRender("%12 %% %xd %M") - p.Render(buf, map[string]interface{}{_level: _infoLevel.String(), _log: "2233"}) - assert.Equal(t, "%12 %% %xd 2233", buf.String()) -} diff --git a/pkg/log/stdout.go b/pkg/log/stdout.go deleted file mode 100644 index ff9630426..000000000 --- a/pkg/log/stdout.go +++ /dev/null @@ -1,53 +0,0 @@ -package log - -import ( - "context" - "os" - "time" -) - -const defaultPattern = "%L %d-%T %f %M" - -var _defaultStdout = NewStdout() - -// StdoutHandler stdout log handler -type StdoutHandler struct { - render Render -} - -// NewStdout create a stdout log handler -func NewStdout() *StdoutHandler { - return &StdoutHandler{render: newPatternRender(defaultPattern)} -} - -// Log stdout loging, only for developing env. -func (h *StdoutHandler) Log(ctx context.Context, lv Level, args ...D) { - d := toMap(args...) - // add extra fields - addExtraField(ctx, d) - d[_time] = time.Now().Format(_timeFormat) - h.render.Render(os.Stderr, d) - os.Stderr.Write([]byte("\n")) -} - -// Close stdout loging -func (h *StdoutHandler) Close() error { - return nil -} - -// SetFormat set stdout log output format -// %T time format at "15:04:05.999" -// %t time format at "15:04:05" -// %D data format at "2006/01/02" -// %d data format at "01/02" -// %L log level e.g. INFO WARN ERROR -// %f function name and line number e.g. model.Get:121 -// %i instance id -// %e deploy env e.g. dev uat fat prod -// %z zone -// %S full file name and line number: /a/b/c/d.go:23 -// %s final file name element and line number: d.go:23 -// %M log message and additional fields: key=value this is log message -func (h *StdoutHandler) SetFormat(format string) { - h.render = newPatternRender(format) -} diff --git a/pkg/log/util.go b/pkg/log/util.go deleted file mode 100644 index 4d21fec48..000000000 --- a/pkg/log/util.go +++ /dev/null @@ -1,69 +0,0 @@ -package log - -import ( - "context" - "math" - "runtime" - "strconv" - "time" - - "github.com/go-kratos/kratos/pkg/conf/env" - "github.com/go-kratos/kratos/pkg/log/internal/core" - "github.com/go-kratos/kratos/pkg/net/metadata" - "github.com/go-kratos/kratos/pkg/net/trace" -) - -func addExtraField(ctx context.Context, fields map[string]interface{}) { - if t, ok := trace.FromContext(ctx); ok { - fields[_tid] = t.TraceID() - } - if caller := metadata.String(ctx, metadata.Caller); caller != "" { - fields[_caller] = caller - } - if color := metadata.String(ctx, metadata.Color); color != "" { - fields[_color] = color - } - if env.Color != "" { - fields[_envColor] = env.Color - } - if cluster := metadata.String(ctx, metadata.Cluster); cluster != "" { - fields[_cluster] = cluster - } - fields[_deplyEnv] = env.DeployEnv - fields[_zone] = env.Zone - fields[_appID] = c.Family - fields[_instanceID] = c.Host - if metadata.String(ctx, metadata.Mirror) != "" { - fields[_mirror] = true - } -} - -// funcName get func name. -func funcName(skip int) (name string) { - if _, file, lineNo, ok := runtime.Caller(skip); ok { - return file + ":" + strconv.Itoa(lineNo) - } - return "unknown:0" -} - -// toMap convert D slice to map[string]interface{} for legacy file and stdout. -func toMap(args ...D) map[string]interface{} { - d := make(map[string]interface{}, 10+len(args)) - for _, arg := range args { - switch arg.Type { - case core.UintType, core.Uint64Type, core.IntTpye, core.Int64Type: - d[arg.Key] = arg.Int64Val - case core.StringType: - d[arg.Key] = arg.StringVal - case core.Float32Type: - d[arg.Key] = math.Float32frombits(uint32(arg.Int64Val)) - case core.Float64Type: - d[arg.Key] = math.Float64frombits(uint64(arg.Int64Val)) - case core.DurationType: - d[arg.Key] = time.Duration(arg.Int64Val) - default: - d[arg.Key] = arg.Value - } - } - return d -} diff --git a/pkg/log/util_test.go b/pkg/log/util_test.go deleted file mode 100644 index 546fd6b1c..000000000 --- a/pkg/log/util_test.go +++ /dev/null @@ -1,54 +0,0 @@ -package log - -import ( - "reflect" - "strings" - "testing" - "time" -) - -func TestFuncName(t *testing.T) { - name := funcName(1) - if !strings.Contains(name, "util_test.go:11") { - t.Errorf("expect contains util_test.go:11 got %s", name) - } -} - -func Test_toMap(t *testing.T) { - type args struct { - args []D - } - tests := []struct { - name string - args args - want map[string]interface{} - }{ - { - args: args{[]D{KVString("test", "hello")}}, - want: map[string]interface{}{"test": "hello"}, - }, - { - args: args{[]D{KVInt64("test", 123)}}, - want: map[string]interface{}{"test": int64(123)}, - }, - { - args: args{[]D{KVFloat32("test", float32(1.01))}}, - want: map[string]interface{}{"test": float32(1.01)}, - }, - { - args: args{[]D{KVFloat32("test", float32(1.01))}}, - want: map[string]interface{}{"test": float32(1.01)}, - }, - { - args: args{[]D{KVDuration("test", time.Second)}}, - want: map[string]interface{}{"test": time.Second}, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - if got := toMap(tt.args.args...); !reflect.DeepEqual(got, tt.want) { - t.Errorf("toMap() = %v, want %v", got, tt.want) - } - }) - } -} diff --git a/pkg/log/verbose.go b/pkg/log/verbose.go deleted file mode 100644 index 44124c6c6..000000000 --- a/pkg/log/verbose.go +++ /dev/null @@ -1,83 +0,0 @@ -package log - -import ( - "context" - "fmt" - "path/filepath" - "runtime" - "strings" -) - -// V reports whether verbosity at the call site is at least the requested level. -// The returned value is a boolean of type Verbose, which implements Info, Infov etc. -// These methods will write to the Info log if called. -// Thus, one may write either -// if log.V(2) { log.Info("log this") } -// or -// log.V(2).Info("log this") -// The second form is shorter but the first is cheaper if logging is off because it does -// not evaluate its arguments. -// -// Whether an individual call to V generates a log record depends on the setting of -// the Config.VLevel and Config.Module flags; both are off by default. If the level in the call to -// V is at least the value of Config.VLevel, or of Config.Module for the source file containing the -// call, the V call will log. -// v must be more than 0. -func V(v int32) Verbose { - var ( - file string - ) - if v < 0 { - return Verbose(false) - } else if c.V >= v { - return Verbose(true) - } - if pc, _, _, ok := runtime.Caller(1); ok { - file, _ = runtime.FuncForPC(pc).FileLine(pc) - } - if strings.HasSuffix(file, ".go") { - file = file[:len(file)-3] - } - if slash := strings.LastIndex(file, "/"); slash >= 0 { - file = file[slash+1:] - } - for filter, lvl := range c.Module { - var match bool - if match = filter == file; !match { - match, _ = filepath.Match(filter, file) - } - if match { - return Verbose(lvl >= v) - } - } - return Verbose(false) -} - -// Info logs a message at the info log level. -func (v Verbose) Info(format string, args ...interface{}) { - if v { - h.Log(context.Background(), _infoLevel, KVString(_log, fmt.Sprintf(format, args...))) - } -} - -// Infov logs a message at the info log level. -func (v Verbose) Infov(ctx context.Context, args ...D) { - if v { - h.Log(ctx, _infoLevel, args...) - } -} - -// Infow logs a message with some additional context. The variadic key-value pairs are treated as they are in With. -func (v Verbose) Infow(ctx context.Context, args ...interface{}) { - if v { - h.Log(ctx, _infoLevel, logw(args)...) - } -} - -// Close close resource. -func (v Verbose) Close() (err error) { - if h == nil { - return - } - return h.Close() -} diff --git a/pkg/naming/README.md b/pkg/naming/README.md deleted file mode 100644 index 959efc91d..000000000 --- a/pkg/naming/README.md +++ /dev/null @@ -1,14 +0,0 @@ -# naming - -## 项目简介 - -服务发现、服务注册相关的SDK集合 - -## 现状 - -目前默认实现了B站开源的[Discovery](https://github.com/bilibili/discovery)服务注册与发现SDK。 -但在使用之前,请确认discovery服务部署完成,并将该discovery.go内`fixConfig`方法的默认配置进行完善。 - -## 使用 - -可实现`naming`内的`Builder`&`Resolver`&`Registry`接口用于服务注册与发现,比如B站内部还实现了zk的。 diff --git a/pkg/naming/discovery/discovery.go b/pkg/naming/discovery/discovery.go deleted file mode 100644 index 21a5de118..000000000 --- a/pkg/naming/discovery/discovery.go +++ /dev/null @@ -1,709 +0,0 @@ -package discovery - -import ( - "context" - "encoding/json" - "errors" - "fmt" - "math/rand" - "net/url" - "strconv" - "strings" - "sync" - "sync/atomic" - "time" - - "github.com/go-kratos/kratos/pkg/conf/env" - "github.com/go-kratos/kratos/pkg/ecode" - "github.com/go-kratos/kratos/pkg/log" - "github.com/go-kratos/kratos/pkg/naming" - http "github.com/go-kratos/kratos/pkg/net/http/blademaster" - xtime "github.com/go-kratos/kratos/pkg/time" -) - -const ( - _registerURL = "http://%s/discovery/register" - _setURL = "http://%s/discovery/set" - _cancelURL = "http://%s/discovery/cancel" - _renewURL = "http://%s/discovery/renew" - _pollURL = "http://%s/discovery/polls" - - _registerGap = 30 * time.Second - - _statusUP = "1" - - _appid = "infra.discovery" -) - -var ( - _ naming.Builder = &Discovery{} - _ naming.Registry = &Discovery{} - _ naming.Resolver = &Resolve{} - - // ErrDuplication duplication treeid. - ErrDuplication = errors.New("discovery: instance duplicate registration") -) - -var ( - _once sync.Once - _builder naming.Builder -) - -// Builder return default discvoery resolver builder. -func Builder() naming.Builder { - _once.Do(func() { - _builder = New(nil) - }) - return _builder -} - -// Build register resolver into default discovery. -func Build(id string) naming.Resolver { - return Builder().Build(id) -} - -// Config discovery configures. -type Config struct { - Nodes []string - Region string - Zone string - Env string - Host string -} - -// Discovery is discovery client. -type Discovery struct { - c *Config - once sync.Once - ctx context.Context - cancelFunc context.CancelFunc - httpClient *http.Client - - node atomic.Value - nodeIdx uint64 - - mutex sync.RWMutex - apps map[string]*appInfo - registry map[string]struct{} - lastHost string - cancelPolls context.CancelFunc - - delete chan *appInfo -} - -type appInfo struct { - resolver map[*Resolve]struct{} - zoneIns atomic.Value - lastTs int64 // latest timestamp -} - -func fixConfig(c *Config) error { - if len(c.Nodes) == 0 && env.DiscoveryNodes != "" { - c.Nodes = strings.Split(env.DiscoveryNodes, ",") - } - if c.Region == "" { - c.Region = env.Region - } - if c.Zone == "" { - c.Zone = env.Zone - } - if c.Env == "" { - c.Env = env.DeployEnv - } - if c.Host == "" { - c.Host = env.Hostname - } - if len(c.Nodes) == 0 || c.Region == "" || c.Zone == "" || c.Env == "" || c.Host == "" { - return fmt.Errorf( - "invalid discovery config nodes:%+v region:%s zone:%s deployEnv:%s host:%s", - c.Nodes, - c.Region, - c.Zone, - c.Env, - c.Host, - ) - } - return nil -} - -// New new a discovery client. -func New(c *Config) (d *Discovery) { - if c == nil { - c = new(Config) - } - if err := fixConfig(c); err != nil { - panic(err) - } - ctx, cancel := context.WithCancel(context.Background()) - d = &Discovery{ - c: c, - ctx: ctx, - cancelFunc: cancel, - apps: map[string]*appInfo{}, - registry: map[string]struct{}{}, - delete: make(chan *appInfo, 10), - } - // httpClient - cfg := &http.ClientConfig{ - Dial: xtime.Duration(3 * time.Second), - Timeout: xtime.Duration(40 * time.Second), - KeepAlive: xtime.Duration(40 * time.Second), - } - d.httpClient = http.NewClient(cfg) - // discovery self - resolver := d.Build(_appid) - event := resolver.Watch() - _, ok := <-event - if !ok { - panic("discovery watch failed") - } - ins, ok := resolver.Fetch(context.Background()) - if ok { - d.newSelf(ins.Instances) - } - go d.selfproc(resolver, event) - return -} - -func (d *Discovery) selfproc(resolver naming.Resolver, event <-chan struct{}) { - for { - _, ok := <-event - if !ok { - return - } - zones, ok := resolver.Fetch(context.Background()) - if ok { - d.newSelf(zones.Instances) - } - } -} - -func (d *Discovery) newSelf(zones map[string][]*naming.Instance) { - ins, ok := zones[d.c.Zone] - if !ok { - return - } - var nodes []string - for _, in := range ins { - for _, addr := range in.Addrs { - u, err := url.Parse(addr) - if err == nil && u.Scheme == "http" { - nodes = append(nodes, u.Host) - } - } - } - // diff old nodes - var olds int - for _, n := range nodes { - if node, ok := d.node.Load().([]string); ok { - for _, o := range node { - if o == n { - olds++ - break - } - } - } - } - if len(nodes) == olds { - return - } - // FIXME: we should use rand.Shuffle() in golang 1.10 - shuffle(len(nodes), func(i, j int) { - nodes[i], nodes[j] = nodes[j], nodes[i] - }) - d.node.Store(nodes) -} - -// Build disovery resovler builder. -func (d *Discovery) Build(appid string, opts ...naming.BuildOpt) naming.Resolver { - r := &Resolve{ - id: appid, - d: d, - event: make(chan struct{}, 1), - opt: new(naming.BuildOptions), - } - for _, opt := range opts { - opt.Apply(r.opt) - } - d.mutex.Lock() - app, ok := d.apps[appid] - if !ok { - app = &appInfo{ - resolver: make(map[*Resolve]struct{}), - } - d.apps[appid] = app - cancel := d.cancelPolls - if cancel != nil { - cancel() - } - } - app.resolver[r] = struct{}{} - d.mutex.Unlock() - if ok { - select { - case r.event <- struct{}{}: - default: - } - } - log.Info("disocvery: AddWatch(%s) already watch(%v)", appid, ok) - d.once.Do(func() { - go d.serverproc() - }) - return r -} - -// Scheme return discovery's scheme -func (d *Discovery) Scheme() string { - return "discovery" -} - -// Resolve discveory resolver. -type Resolve struct { - id string - event chan struct{} - d *Discovery - opt *naming.BuildOptions -} - -// Watch watch instance. -func (r *Resolve) Watch() <-chan struct{} { - return r.event -} - -// Fetch fetch resolver instance. -func (r *Resolve) Fetch(ctx context.Context) (ins *naming.InstancesInfo, ok bool) { - r.d.mutex.RLock() - app, ok := r.d.apps[r.id] - r.d.mutex.RUnlock() - if ok { - var appIns *naming.InstancesInfo - appIns, ok = app.zoneIns.Load().(*naming.InstancesInfo) - if !ok { - return - } - ins = new(naming.InstancesInfo) - ins.LastTs = appIns.LastTs - ins.Scheduler = appIns.Scheduler - if r.opt.Filter != nil { - ins.Instances = r.opt.Filter(appIns.Instances) - } else { - ins.Instances = make(map[string][]*naming.Instance) - for zone, in := range appIns.Instances { - ins.Instances[zone] = in - } - } - if r.opt.Scheduler != nil { - ins.Instances[r.opt.ClientZone] = r.opt.Scheduler(ins) - } - if r.opt.Subset != nil && r.opt.SubsetSize != 0 { - for zone, inss := range ins.Instances { - ins.Instances[zone] = r.opt.Subset(inss, r.opt.SubsetSize) - } - } - } - return -} - -// Close close resolver. -func (r *Resolve) Close() error { - r.d.mutex.Lock() - if app, ok := r.d.apps[r.id]; ok && len(app.resolver) != 0 { - delete(app.resolver, r) - // TODO: delete app from builder - } - r.d.mutex.Unlock() - return nil -} - -// Reload reload the config -func (d *Discovery) Reload(c *Config) { - fixConfig(c) - d.mutex.Lock() - d.c = c - d.mutex.Unlock() -} - -// Close stop all running process including discovery and register -func (d *Discovery) Close() error { - d.cancelFunc() - return nil -} - -// Register Register an instance with discovery and renew automatically -func (d *Discovery) Register(ctx context.Context, ins *naming.Instance) (cancelFunc context.CancelFunc, err error) { - d.mutex.Lock() - if _, ok := d.registry[ins.AppID]; ok { - err = ErrDuplication - } else { - d.registry[ins.AppID] = struct{}{} - } - d.mutex.Unlock() - if err != nil { - return - } - - ctx, cancel := context.WithCancel(d.ctx) - if err = d.register(ctx, ins); err != nil { - d.mutex.Lock() - delete(d.registry, ins.AppID) - d.mutex.Unlock() - cancel() - return - } - ch := make(chan struct{}, 1) - cancelFunc = context.CancelFunc(func() { - cancel() - <-ch - }) - go func() { - ticker := time.NewTicker(_registerGap) - defer ticker.Stop() - for { - select { - case <-ticker.C: - if err := d.renew(ctx, ins); err != nil && ecode.EqualError(ecode.NothingFound, err) { - _ = d.register(ctx, ins) - } - case <-ctx.Done(): - _ = d.cancel(ins) - ch <- struct{}{} - return - } - } - }() - return -} - -// register Register an instance with discovery -func (d *Discovery) register(ctx context.Context, ins *naming.Instance) (err error) { - d.mutex.RLock() - c := d.c - d.mutex.RUnlock() - - var metadata []byte - if ins.Metadata != nil { - if metadata, err = json.Marshal(ins.Metadata); err != nil { - log.Error("discovery:register instance Marshal metadata(%v) failed!error(%v)", ins.Metadata, err) - } - } - res := new(struct { - Code int `json:"code"` - Message string `json:"message"` - }) - uri := fmt.Sprintf(_registerURL, d.pickNode()) - params := d.newParams(c) - params.Set("appid", ins.AppID) - for _, addr := range ins.Addrs { - params.Add("addrs", addr) - } - params.Set("version", ins.Version) - if ins.Status == 0 { - params.Set("status", _statusUP) - } else { - params.Set("status", strconv.FormatInt(ins.Status, 10)) - } - params.Set("metadata", string(metadata)) - if err = d.httpClient.Post(ctx, uri, "", params, &res); err != nil { - d.switchNode() - log.Error("discovery: register client.Get(%v) zone(%s) env(%s) appid(%s) addrs(%v) error(%v)", - uri, c.Zone, c.Env, ins.AppID, ins.Addrs, err) - return - } - if ec := ecode.Int(res.Code); !ecode.Equal(ecode.OK, ec) { - log.Warn("discovery: register client.Get(%v) env(%s) appid(%s) addrs(%v) code(%v)", uri, c.Env, ins.AppID, ins.Addrs, res.Code) - err = ec - return - } - log.Info("discovery: register client.Get(%v) env(%s) appid(%s) addrs(%s) success", uri, c.Env, ins.AppID, ins.Addrs) - return -} - -// renew Renew an instance with discovery -func (d *Discovery) renew(ctx context.Context, ins *naming.Instance) (err error) { - d.mutex.RLock() - c := d.c - d.mutex.RUnlock() - - res := new(struct { - Code int `json:"code"` - Message string `json:"message"` - }) - uri := fmt.Sprintf(_renewURL, d.pickNode()) - params := d.newParams(c) - params.Set("appid", ins.AppID) - if err = d.httpClient.Post(ctx, uri, "", params, &res); err != nil { - d.switchNode() - log.Error("discovery: renew client.Get(%v) env(%s) appid(%s) hostname(%s) error(%v)", - uri, c.Env, ins.AppID, c.Host, err) - return - } - if ec := ecode.Int(res.Code); !ecode.Equal(ecode.OK, ec) { - err = ec - if ecode.Equal(ecode.NothingFound, ec) { - return - } - log.Error("discovery: renew client.Get(%v) env(%s) appid(%s) hostname(%s) code(%v)", - uri, c.Env, ins.AppID, c.Host, res.Code) - return - } - return -} - -// cancel Remove the registered instance from discovery -func (d *Discovery) cancel(ins *naming.Instance) (err error) { - d.mutex.RLock() - c := d.c - d.mutex.RUnlock() - - res := new(struct { - Code int `json:"code"` - Message string `json:"message"` - }) - uri := fmt.Sprintf(_cancelURL, d.pickNode()) - params := d.newParams(c) - params.Set("appid", ins.AppID) - // request - if err = d.httpClient.Post(context.TODO(), uri, "", params, &res); err != nil { - d.switchNode() - log.Error("discovery cancel client.Get(%v) env(%s) appid(%s) hostname(%s) error(%v)", - uri, c.Env, ins.AppID, c.Host, err) - return - } - if ec := ecode.Int(res.Code); !ecode.Equal(ecode.OK, ec) { - log.Warn("discovery cancel client.Get(%v) env(%s) appid(%s) hostname(%s) code(%v)", - uri, c.Env, ins.AppID, c.Host, res.Code) - err = ec - return - } - log.Info("discovery cancel client.Get(%v) env(%s) appid(%s) hostname(%s) success", - uri, c.Env, ins.AppID, c.Host) - return -} - -// Set set ins status and metadata. -func (d *Discovery) Set(ins *naming.Instance) error { - return d.set(context.Background(), ins) -} - -// set set instance info with discovery -func (d *Discovery) set(ctx context.Context, ins *naming.Instance) (err error) { - d.mutex.RLock() - conf := d.c - d.mutex.RUnlock() - res := new(struct { - Code int `json:"code"` - Message string `json:"message"` - }) - uri := fmt.Sprintf(_setURL, d.pickNode()) - params := d.newParams(conf) - params.Set("appid", ins.AppID) - params.Set("version", ins.Version) - params.Set("status", strconv.FormatInt(ins.Status, 10)) - if ins.Metadata != nil { - var metadata []byte - if metadata, err = json.Marshal(ins.Metadata); err != nil { - log.Error("discovery:set instance Marshal metadata(%v) failed!error(%v)", ins.Metadata, err) - return - } - params.Set("metadata", string(metadata)) - } - if err = d.httpClient.Post(ctx, uri, "", params, &res); err != nil { - d.switchNode() - log.Error("discovery: set client.Get(%v) zone(%s) env(%s) appid(%s) addrs(%v) error(%v)", - uri, conf.Zone, conf.Env, ins.AppID, ins.Addrs, err) - return - } - if ec := ecode.Int(res.Code); !ecode.Equal(ecode.OK, ec) { - log.Warn("discovery: set client.Get(%v) env(%s) appid(%s) addrs(%v) code(%v)", - uri, conf.Env, ins.AppID, ins.Addrs, res.Code) - err = ec - return - } - log.Info("discovery: set client.Get(%v) env(%s) appid(%s) addrs(%s) success", uri+"?"+params.Encode(), conf.Env, ins.AppID, ins.Addrs) - return -} - -func (d *Discovery) serverproc() { - var ( - retry int - ctx context.Context - cancel context.CancelFunc - ) - ticker := time.NewTicker(time.Minute * 30) - defer ticker.Stop() - for { - if ctx == nil { - ctx, cancel = context.WithCancel(d.ctx) - d.mutex.Lock() - d.cancelPolls = cancel - d.mutex.Unlock() - } - select { - case <-d.ctx.Done(): - return - case <-ticker.C: - d.switchNode() - default: - } - apps, err := d.polls(ctx) - if err != nil { - d.switchNode() - if ctx.Err() == context.Canceled { - ctx = nil - continue - } - time.Sleep(time.Second) - retry++ - continue - } - retry = 0 - d.broadcast(apps) - } -} - -func (d *Discovery) pickNode() string { - nodes, ok := d.node.Load().([]string) - if !ok || len(nodes) == 0 { - return d.c.Nodes[rand.Intn(len(d.c.Nodes))] - } - return nodes[atomic.LoadUint64(&d.nodeIdx)%uint64(len(nodes))] -} - -func (d *Discovery) switchNode() { - atomic.AddUint64(&d.nodeIdx, 1) -} - -func (d *Discovery) polls(ctx context.Context) (apps map[string]*naming.InstancesInfo, err error) { - var ( - lastTss []int64 - appIDs []string - host = d.pickNode() - changed bool - ) - if host != d.lastHost { - d.lastHost = host - changed = true - } - d.mutex.RLock() - c := d.c - for k, v := range d.apps { - if changed { - v.lastTs = 0 - } - appIDs = append(appIDs, k) - lastTss = append(lastTss, v.lastTs) - } - d.mutex.RUnlock() - if len(appIDs) == 0 { - return - } - uri := fmt.Sprintf(_pollURL, host) - res := new(struct { - Code int `json:"code"` - Data map[string]*naming.InstancesInfo `json:"data"` - }) - params := url.Values{} - params.Set("env", c.Env) - params.Set("hostname", c.Host) - for _, appid := range appIDs { - params.Add("appid", appid) - } - for _, ts := range lastTss { - params.Add("latest_timestamp", strconv.FormatInt(ts, 10)) - } - if err = d.httpClient.Get(ctx, uri, "", params, res); err != nil { - d.switchNode() - if ctx.Err() != context.Canceled { - log.Error("discovery: client.Get(%s) error(%+v)", uri+"?"+params.Encode(), err) - } - return - } - if ec := ecode.Int(res.Code); !ecode.Equal(ecode.OK, ec) { - if !ecode.Equal(ecode.NotModified, ec) { - log.Error("discovery: client.Get(%s) get error code(%d)", uri+"?"+params.Encode(), res.Code) - err = ec - } - return - } - info, _ := json.Marshal(res.Data) - for _, app := range res.Data { - if app.LastTs == 0 { - err = ecode.ServerErr - log.Error("discovery: client.Get(%s) latest_timestamp is 0,instances:(%s)", uri+"?"+params.Encode(), info) - return - } - } - log.Info("discovery: successfully polls(%s) instances (%s)", uri+"?"+params.Encode(), info) - apps = res.Data - return -} - -func (d *Discovery) broadcast(apps map[string]*naming.InstancesInfo) { - for appID, v := range apps { - var count int - // v maybe nil in old version(less than v1.1) discovery,check incase of panic - if v == nil { - continue - } - for zone, ins := range v.Instances { - if len(ins) == 0 { - delete(v.Instances, zone) - } - count += len(ins) - } - if count == 0 { - continue - } - d.mutex.RLock() - app, ok := d.apps[appID] - d.mutex.RUnlock() - if ok { - app.lastTs = v.LastTs - app.zoneIns.Store(v) - d.mutex.RLock() - for rs := range app.resolver { - select { - case rs.event <- struct{}{}: - default: - } - } - d.mutex.RUnlock() - } - } -} - -func (d *Discovery) newParams(c *Config) url.Values { - params := url.Values{} - params.Set("region", c.Region) - params.Set("zone", c.Zone) - params.Set("env", c.Env) - params.Set("hostname", c.Host) - return params -} - -var r = rand.New(rand.NewSource(time.Now().UnixNano())) - -// shuffle pseudo-randomizes the order of elements. -// n is the number of elements. Shuffle panics if n < 0. -// swap swaps the elements with indexes i and j. -func shuffle(n int, swap func(i, j int)) { - if n < 0 { - panic("invalid argument to Shuffle") - } - - // Fisher-Yates shuffle: https://en.wikipedia.org/wiki/Fisher%E2%80%93Yates_shuffle - // Shuffle really ought not be called with n that doesn't fit in 32 bits. - // Not only will it take a very long time, but with 2³¹! possible permutations, - // there's no way that any PRNG can have a big enough internal state to - // generate even a minuscule percentage of the possible permutations. - // Nevertheless, the right API signature accepts an int n, so handle it as best we can. - i := n - 1 - for ; i > 1<<31-1-1; i-- { - j := int(r.Int63n(int64(i + 1))) - swap(i, j) - } - for ; i > 0; i-- { - j := int(r.Int31n(int32(i + 1))) - swap(i, j) - } -} diff --git a/pkg/naming/etcd/etcd.go b/pkg/naming/etcd/etcd.go deleted file mode 100644 index 9271cd276..000000000 --- a/pkg/naming/etcd/etcd.go +++ /dev/null @@ -1,324 +0,0 @@ -package etcd - -import ( - "context" - "encoding/json" - "errors" - "flag" - "fmt" - "os" - "strings" - "sync" - "sync/atomic" - "time" - - "go.etcd.io/etcd/clientv3" - "go.etcd.io/etcd/mvcc/mvccpb" - "google.golang.org/grpc" - - "github.com/go-kratos/kratos/pkg/log" - "github.com/go-kratos/kratos/pkg/naming" -) - -var ( - //etcdPrefix is a etcd globe key prefix - endpoints string - etcdPrefix string - - //Time units is second - registerTTL = 90 - defaultDialTimeout = 30 -) - -var ( - _once sync.Once - _builder naming.Builder - //ErrDuplication is a register duplication err - ErrDuplication = errors.New("etcd: instance duplicate registration") -) - -func init() { - addFlag(flag.CommandLine) -} - -func addFlag(fs *flag.FlagSet) { - // env - fs.StringVar(&endpoints, "etcd.endpoints", os.Getenv("ETCD_ENDPOINTS"), "etcd.endpoints is etcd endpoints. value: 127.0.0.1:2379,127.0.0.2:2379 etc.") - fs.StringVar(&etcdPrefix, "etcd.prefix", defaultString("ETCD_PREFIX", "kratos_etcd"), "etcd globe key prefix or use ETCD_PREFIX env variable. value etcd_prefix etc.") -} - -func defaultString(env, value string) string { - v := os.Getenv(env) - if v == "" { - return value - } - return v -} - -// Builder return default etcd resolver builder. -func Builder(c *clientv3.Config) naming.Builder { - _once.Do(func() { - _builder, _ = New(c) - }) - return _builder -} - -// Build register resolver into default etcd. -func Build(c *clientv3.Config, id string) naming.Resolver { - return Builder(c).Build(id) -} - -// EtcdBuilder is a etcd clientv3 EtcdBuilder -type EtcdBuilder struct { - cli *clientv3.Client - ctx context.Context - cancelFunc context.CancelFunc - - mutex sync.RWMutex - apps map[string]*appInfo - registry map[string]struct{} -} -type appInfo struct { - resolver map[*Resolve]struct{} - ins atomic.Value - e *EtcdBuilder - once sync.Once -} - -// Resolve etch resolver. -type Resolve struct { - id string - event chan struct{} - e *EtcdBuilder - opt *naming.BuildOptions -} - -// New is new a etcdbuilder -func New(c *clientv3.Config) (e *EtcdBuilder, err error) { - if c == nil { - if endpoints == "" { - panic(fmt.Errorf("invalid etcd config endpoints:%+v", endpoints)) - } - c = &clientv3.Config{ - Endpoints: strings.Split(endpoints, ","), - DialTimeout: time.Second * time.Duration(defaultDialTimeout), - DialOptions: []grpc.DialOption{grpc.WithBlock()}, - } - } - cli, err := clientv3.New(*c) - if err != nil { - return nil, err - } - ctx, cancel := context.WithCancel(context.Background()) - e = &EtcdBuilder{ - cli: cli, - ctx: ctx, - cancelFunc: cancel, - apps: map[string]*appInfo{}, - registry: map[string]struct{}{}, - } - return -} - -// Build disovery resovler builder. -func (e *EtcdBuilder) Build(appid string, opts ...naming.BuildOpt) naming.Resolver { - r := &Resolve{ - id: appid, - e: e, - event: make(chan struct{}, 1), - opt: new(naming.BuildOptions), - } - e.mutex.Lock() - app, ok := e.apps[appid] - if !ok { - app = &appInfo{ - resolver: make(map[*Resolve]struct{}), - e: e, - } - e.apps[appid] = app - } - app.resolver[r] = struct{}{} - e.mutex.Unlock() - if ok { - select { - case r.event <- struct{}{}: - default: - } - } - - app.once.Do(func() { - go app.watch(appid) - log.Info("etcd: AddWatch(%s) already watch(%v)", appid, ok) - }) - return r -} - -// Scheme return etcd's scheme -func (e *EtcdBuilder) Scheme() string { - return "etcd" -} - -// Register is register instance -func (e *EtcdBuilder) Register(ctx context.Context, ins *naming.Instance) (cancelFunc context.CancelFunc, err error) { - e.mutex.Lock() - if _, ok := e.registry[ins.AppID]; ok { - err = ErrDuplication - } else { - e.registry[ins.AppID] = struct{}{} - } - e.mutex.Unlock() - if err != nil { - return - } - ctx, cancel := context.WithCancel(e.ctx) - if err = e.register(ctx, ins); err != nil { - e.mutex.Lock() - delete(e.registry, ins.AppID) - e.mutex.Unlock() - cancel() - return - } - ch := make(chan struct{}, 1) - cancelFunc = context.CancelFunc(func() { - cancel() - <-ch - }) - - go func() { - ticker := time.NewTicker(time.Duration(registerTTL/3) * time.Second) - defer ticker.Stop() - for { - select { - case <-ticker.C: - _ = e.register(ctx, ins) - case <-ctx.Done(): - _ = e.unregister(ins) - ch <- struct{}{} - return - } - } - }() - return -} - -//注册和续约公用一个操作 -func (e *EtcdBuilder) register(ctx context.Context, ins *naming.Instance) (err error) { - prefix := e.keyPrefix(ins) - val, _ := json.Marshal(ins) - - ttlResp, err := e.cli.Grant(context.TODO(), int64(registerTTL)) - if err != nil { - log.Error("etcd: register client.Grant(%v) error(%v)", registerTTL, err) - return err - } - _, err = e.cli.Put(ctx, prefix, string(val), clientv3.WithLease(ttlResp.ID)) - if err != nil { - log.Error("etcd: register client.Put(%v) appid(%s) hostname(%s) error(%v)", - prefix, ins.AppID, ins.Hostname, err) - return err - } - return nil -} -func (e *EtcdBuilder) unregister(ins *naming.Instance) (err error) { - prefix := e.keyPrefix(ins) - - if _, err = e.cli.Delete(context.TODO(), prefix); err != nil { - log.Error("etcd: unregister client.Delete(%v) appid(%s) hostname(%s) error(%v)", - prefix, ins.AppID, ins.Hostname, err) - } - log.Info("etcd: unregister client.Delete(%v) appid(%s) hostname(%s) success", - prefix, ins.AppID, ins.Hostname) - return -} - -func (e *EtcdBuilder) keyPrefix(ins *naming.Instance) string { - return fmt.Sprintf("/%s/%s/%s", etcdPrefix, ins.AppID, ins.Hostname) -} - -// Close stop all running process including etcdfetch and register -func (e *EtcdBuilder) Close() error { - e.cancelFunc() - return nil -} -func (a *appInfo) watch(appID string) { - _ = a.fetchstore(appID) - prefix := fmt.Sprintf("/%s/%s/", etcdPrefix, appID) - rch := a.e.cli.Watch(a.e.ctx, prefix, clientv3.WithPrefix()) - for wresp := range rch { - for _, ev := range wresp.Events { - if ev.Type == mvccpb.PUT || ev.Type == mvccpb.DELETE { - _ = a.fetchstore(appID) - } - } - } -} - -func (a *appInfo) fetchstore(appID string) (err error) { - prefix := fmt.Sprintf("/%s/%s/", etcdPrefix, appID) - resp, err := a.e.cli.Get(a.e.ctx, prefix, clientv3.WithPrefix()) - if err != nil { - log.Error("etcd: fetch client.Get(%s) error(%+v)", prefix, err) - return err - } - - ins, err := a.paserIns(resp) - if err != nil { - return err - } - a.store(ins) - return nil -} -func (a *appInfo) store(ins *naming.InstancesInfo) { - a.ins.Store(ins) - a.e.mutex.RLock() - for rs := range a.resolver { - select { - case rs.event <- struct{}{}: - default: - } - } - a.e.mutex.RUnlock() -} - -func (a *appInfo) paserIns(resp *clientv3.GetResponse) (ins *naming.InstancesInfo, err error) { - ins = &naming.InstancesInfo{ - Instances: make(map[string][]*naming.Instance), - } - for _, ev := range resp.Kvs { - in := new(naming.Instance) - - err := json.Unmarshal(ev.Value, in) - if err != nil { - return nil, err - } - ins.Instances[in.Zone] = append(ins.Instances[in.Zone], in) - } - return ins, nil -} - -// Watch watch instance. -func (r *Resolve) Watch() <-chan struct{} { - return r.event -} - -// Fetch fetch resolver instance. -func (r *Resolve) Fetch(ctx context.Context) (ins *naming.InstancesInfo, ok bool) { - r.e.mutex.RLock() - app, ok := r.e.apps[r.id] - r.e.mutex.RUnlock() - if ok { - ins, ok = app.ins.Load().(*naming.InstancesInfo) - return - } - return -} - -// Close close resolver. -func (r *Resolve) Close() error { - r.e.mutex.Lock() - if app, ok := r.e.apps[r.id]; ok && len(app.resolver) != 0 { - delete(app.resolver, r) - } - r.e.mutex.Unlock() - return nil -} diff --git a/pkg/naming/naming.go b/pkg/naming/naming.go deleted file mode 100644 index 936faf626..000000000 --- a/pkg/naming/naming.go +++ /dev/null @@ -1,80 +0,0 @@ -package naming - -import ( - "context" -) - -// metadata common key -const ( - MetaWeight = "weight" - MetaCluster = "cluster" - MetaZone = "zone" - MetaColor = "color" -) - -// Instance represents a server the client connects to. -type Instance struct { - // Region is region. - Region string `json:"region"` - // Zone is IDC. - Zone string `json:"zone"` - // Env prod/pre、uat/fat1 - Env string `json:"env"` - // AppID is mapping servicetree appid. - AppID string `json:"appid"` - // Hostname is hostname from docker. - Hostname string `json:"hostname"` - // Addrs is the address of app instance - // format: scheme://host - Addrs []string `json:"addrs"` - // Version is publishing version. - Version string `json:"version"` - // LastTs is instance latest updated timestamp - LastTs int64 `json:"latest_timestamp"` - // Metadata is the information associated with Addr, which may be used - // to make load balancing decision. - Metadata map[string]string `json:"metadata"` - // Status instance status, eg: 1UP 2Waiting - Status int64 `json:"status"` -} - -// Resolver resolve naming service -type Resolver interface { - Fetch(context.Context) (*InstancesInfo, bool) - Watch() <-chan struct{} - Close() error -} - -// Registry Register an instance and renew automatically. -type Registry interface { - Register(ctx context.Context, ins *Instance) (cancel context.CancelFunc, err error) - Close() error -} - -// Builder resolver builder. -type Builder interface { - Build(id string, options ...BuildOpt) Resolver - Scheme() string -} - -// InstancesInfo instance info. -type InstancesInfo struct { - Instances map[string][]*Instance `json:"instances"` - LastTs int64 `json:"latest_timestamp"` - Scheduler *Scheduler `json:"scheduler"` -} - -// Scheduler scheduler. -type Scheduler struct { - Clients map[string]*ZoneStrategy `json:"clients"` -} - -// ZoneStrategy is the scheduling strategy of all zones -type ZoneStrategy struct { - Zones map[string]*Strategy `json:"zones"` -} - -// Strategy is zone scheduling strategy. -type Strategy struct { - Weight int64 `json:"weight"` -} diff --git a/pkg/naming/opt.go b/pkg/naming/opt.go deleted file mode 100644 index 9f1642339..000000000 --- a/pkg/naming/opt.go +++ /dev/null @@ -1,176 +0,0 @@ -package naming - -import ( - "encoding/json" - "fmt" - "math/rand" - "net/url" - "os" - "sort" - - "github.com/go-kratos/kratos/pkg/conf/env" - "github.com/go-kratos/kratos/pkg/log" - - "github.com/dgryski/go-farm" -) - -// BuildOptions build options. -type BuildOptions struct { - Filter func(map[string][]*Instance) map[string][]*Instance - Subset func([]*Instance, int) []*Instance - SubsetSize int - ClientZone string - Scheduler func(*InstancesInfo) []*Instance -} - -// BuildOpt build option interface. -type BuildOpt interface { - Apply(*BuildOptions) -} - -type funcOpt struct { - f func(*BuildOptions) -} - -func (f *funcOpt) Apply(opt *BuildOptions) { - f.f(opt) -} - -// Filter filter option. -func Filter(schema string, clusters map[string]struct{}) BuildOpt { - return &funcOpt{f: func(opt *BuildOptions) { - opt.Filter = func(inss map[string][]*Instance) map[string][]*Instance { - newInss := make(map[string][]*Instance) - for zone := range inss { - var instances []*Instance - for _, ins := range inss[zone] { - //如果r.clusters的长度大于0说明需要进行集群选择 - if len(clusters) > 0 { - if _, ok := clusters[ins.Metadata[MetaCluster]]; !ok { - continue - } - } - var addr string - for _, a := range ins.Addrs { - u, err := url.Parse(a) - if err == nil && u.Scheme == schema { - addr = u.Host - } - } - if addr == "" { - fmt.Fprintf(os.Stderr, "resolver: app(%s,%s) no valid grpc address(%v) found!", ins.AppID, ins.Hostname, ins.Addrs) - log.Warn("resolver: invalid rpc address(%s,%s,%v) found!", ins.AppID, ins.Hostname, ins.Addrs) - continue - } - instances = append(instances, ins) - } - newInss[zone] = instances - } - return newInss - } - }} -} - -func defulatSubset(inss []*Instance, size int) []*Instance { - backends := inss - if len(backends) <= size { - return backends - } - clientID := env.Hostname - sort.Slice(backends, func(i, j int) bool { - return backends[i].Hostname < backends[j].Hostname - }) - count := len(backends) / size - // hash得到ID - id := farm.Fingerprint64([]byte(clientID)) - // 获得rand轮数 - round := int64(id / uint64(count)) - - s := rand.NewSource(round) - ra := rand.New(s) - // 根据source洗牌 - ra.Shuffle(len(backends), func(i, j int) { - backends[i], backends[j] = backends[j], backends[i] - }) - start := (id % uint64(count)) * uint64(size) - return backends[int(start) : int(start)+size] -} - -// Subset Subset option. -func Subset(defaultSize int) BuildOpt { - return &funcOpt{f: func(opt *BuildOptions) { - opt.SubsetSize = defaultSize - opt.Subset = defulatSubset - }} -} - -// ScheduleNode ScheduleNode option. -func ScheduleNode(clientZone string) BuildOpt { - return &funcOpt{f: func(opt *BuildOptions) { - opt.ClientZone = clientZone - opt.Scheduler = func(app *InstancesInfo) (instances []*Instance) { - type Zone struct { - inss []*Instance - weight int64 - name string - score float64 - } - var zones []*Zone - - if app.Scheduler != nil { - si, err := json.Marshal(app.Scheduler) - if err == nil { - log.Info("schedule info: %s", string(si)) - } - if strategy, ok := app.Scheduler.Clients[clientZone]; ok { - var min *Zone - for name, zone := range strategy.Zones { - inss := app.Instances[name] - if len(inss) == 0 { - continue - } - z := &Zone{ - inss: inss, - weight: zone.Weight, - name: name, - score: float64(len(inss)) / float64(zone.Weight), - } - if min == nil || z.score < min.score { - min = z - } - zones = append(zones, z) - } - if opt.SubsetSize != 0 && len(min.inss) > opt.SubsetSize { - min.score = float64(opt.SubsetSize) / float64(min.weight) - } - for _, z := range zones { - nums := int(min.score * float64(z.weight)) - if nums == 0 { - nums = 1 - } - if nums < len(z.inss) { - if opt.Subset != nil { - z.inss = opt.Subset(z.inss, nums) - } else { - z.inss = defulatSubset(z.inss, nums) - } - } - } - } - } - for _, zone := range zones { - instances = append(instances, zone.inss...) - } - //如果没有拿到节点,则选择直接获取 - if len(instances) == 0 { - instances = app.Instances[clientZone] - if len(instances) == 0 { - for _, value := range app.Instances { - instances = append(instances, value...) - } - } - } - return - } - }} -} diff --git a/pkg/naming/opt_test.go b/pkg/naming/opt_test.go deleted file mode 100644 index a1631c63e..000000000 --- a/pkg/naming/opt_test.go +++ /dev/null @@ -1,299 +0,0 @@ -package naming - -import ( - "fmt" - "reflect" - "testing" -) - -func Test_Subset(t *testing.T) { - var inss1 []*Instance - for i := 0; i < 200; i++ { - ins := &Instance{ - Addrs: []string{fmt.Sprintf("grpc://127.0.0.%d:9000", i)}, - Metadata: map[string]string{MetaCluster: "c1"}, - } - inss1 = append(inss1, ins) - } - var opt BuildOptions - s := Subset(50) - s.Apply(&opt) - sub1 := opt.Subset(inss1, opt.SubsetSize) - if len(sub1) != 50 { - t.Fatalf("subset size should be 50") - } - sub2 := opt.Subset(inss1, opt.SubsetSize) - if !reflect.DeepEqual(sub1, sub2) { - t.Fatalf("two subsets should equal") - } -} - -func Test_FilterClusters(t *testing.T) { - inss := map[string][]*Instance{ - "sh001": {{ - Addrs: []string{"grpc://127.0.0.1:9000"}, - Metadata: map[string]string{MetaCluster: "c1"}, - }, { - Addrs: []string{"http://127.0.0.2:9000"}, - Metadata: map[string]string{MetaCluster: "c1"}, - }, { - Addrs: []string{"grpc://127.0.0.3:9000"}, - Metadata: map[string]string{MetaCluster: "c2"}, - }}, - "sh002": {{ - Addrs: []string{"grpc://127.0.0.1:9000"}, - Metadata: map[string]string{MetaCluster: "c3"}, - }, { - Addrs: []string{"zk://127.0.0.2:9000"}, - Metadata: map[string]string{MetaCluster: "c3"}, - }}, - } - res := map[string][]*Instance{ - "sh001": {{ - Addrs: []string{"grpc://127.0.0.1:9000"}, - Metadata: map[string]string{MetaCluster: "c1"}, - }}, - "sh002": {{ - Addrs: []string{"grpc://127.0.0.1:9000"}, - Metadata: map[string]string{MetaCluster: "c3"}, - }}, - } - var opt BuildOptions - f := Filter("grpc", map[string]struct{}{"c1": {}, "c3": {}}) - f.Apply(&opt) - filtered := opt.Filter(inss) - equal := reflect.DeepEqual(filtered, res) - if !equal { - t.Fatalf("Filter grpc should equal,filtered:%v expected:%v", filtered, res) - } -} - -func Test_FilterInvalidAddr(t *testing.T) { - inss := map[string][]*Instance{ - "sh001": {{ - Addrs: []string{"grpc://127.0.0.1:9000"}, - Metadata: map[string]string{MetaCluster: "c1"}, - }, { - Addrs: []string{"http://127.0.0.2:9000"}, - Metadata: map[string]string{MetaCluster: "c1"}, - }, { - Addrs: []string{"grpc://127.0.0.3:9000"}, - Metadata: map[string]string{MetaCluster: "c2"}, - }}, - "sh002": {{ - Addrs: []string{"grpc://127.0.0.1:9000"}, - Metadata: map[string]string{MetaCluster: "c3"}, - }, { - Addrs: []string{"zk://127.0.0.2:9000"}, - Metadata: map[string]string{MetaCluster: "c3"}, - }}, - } - res := map[string][]*Instance{ - "sh001": {{ - Addrs: []string{"grpc://127.0.0.1:9000"}, - Metadata: map[string]string{MetaCluster: "c1"}, - }, { - Addrs: []string{"grpc://127.0.0.3:9000"}, - Metadata: map[string]string{MetaCluster: "c2"}, - }}, - "sh002": {{ - Addrs: []string{"grpc://127.0.0.1:9000"}, - Metadata: map[string]string{MetaCluster: "c3"}, - }}, - } - var opt BuildOptions - f := Filter("grpc", nil) - f.Apply(&opt) - filtered := opt.Filter(inss) - equal := reflect.DeepEqual(filtered, res) - if !equal { - t.Fatalf("Filter grpc should equal,filtered:%v expected:%v", filtered, res) - } -} - -func Test_Schedule(t *testing.T) { - app := &InstancesInfo{ - Instances: map[string][]*Instance{ - "sh001": {{ - Zone: "sh001", - Addrs: []string{"grpc://127.0.0.1:9000"}, - Metadata: map[string]string{MetaCluster: "c1"}, - }, { - Zone: "sh001", - Addrs: []string{"grpc://127.0.0.2:9000"}, - Metadata: map[string]string{MetaCluster: "c1"}, - }, { - Zone: "sh001", - Addrs: []string{"grpc://127.0.0.3:9000"}, - Metadata: map[string]string{MetaCluster: "c2"}, - }}, - "sh002": {{ - Zone: "sh002", - Addrs: []string{"grpc://127.0.0.1:9000"}, - Metadata: map[string]string{MetaCluster: "c3"}, - }, { - Zone: "sh002", - Addrs: []string{"grpc://127.0.0.2:9000"}, - Metadata: map[string]string{MetaCluster: "c3"}, - }}, - }, - Scheduler: &Scheduler{map[string]*ZoneStrategy{"sh001": { - Zones: map[string]*Strategy{ - "sh001": {10}, - "sh002": {20}, - }, - }}}, - } - var opt BuildOptions - f := ScheduleNode("sh001") - f.Apply(&opt) - err := compareAddr(opt.Scheduler(app), map[string]int{"sh002": 2, "sh001": 1}) - if err != nil { - t.Fatalf(err.Error()) - } -} - -func Test_Schedule2(t *testing.T) { - app := &InstancesInfo{ - Instances: map[string][]*Instance{}, - Scheduler: &Scheduler{map[string]*ZoneStrategy{"sh001": { - Zones: map[string]*Strategy{ - "sh001": {10}, - "sh002": {20}, - }, - }}}, - } - for i := 0; i < 30; i++ { - ins := &Instance{ - Zone: "sh001", - Addrs: []string{fmt.Sprintf("grpc://127.0.0.%d:9000", i)}, - Metadata: map[string]string{MetaCluster: "c1"}, - } - app.Instances[ins.Zone] = append(app.Instances[ins.Zone], ins) - } - for i := 0; i < 30; i++ { - ins := &Instance{ - Zone: "sh002", - Addrs: []string{fmt.Sprintf("grpc://127.0.0.%d:9000", i)}, - Metadata: map[string]string{MetaCluster: "c2"}, - } - app.Instances[ins.Zone] = append(app.Instances[ins.Zone], ins) - } - var opt BuildOptions - f := ScheduleNode("sh001") - f.Apply(&opt) - err := compareAddr(opt.Scheduler(app), map[string]int{"sh002": 30, "sh001": 15}) - if err != nil { - t.Fatalf(err.Error()) - } -} - -func Test_Schedule3(t *testing.T) { - app := &InstancesInfo{ - Instances: map[string][]*Instance{}, - Scheduler: &Scheduler{map[string]*ZoneStrategy{"sh001": { - Zones: map[string]*Strategy{ - "sh001": {1}, - "sh002": {30}, - }, - }}}, - } - for i := 0; i < 30; i++ { - ins := &Instance{ - Zone: "sh001", - Addrs: []string{fmt.Sprintf("grpc://127.0.0.%d:9000", i)}, - Metadata: map[string]string{MetaCluster: "c1"}, - } - app.Instances[ins.Zone] = append(app.Instances[ins.Zone], ins) - } - for i := 0; i < 30; i++ { - ins := &Instance{ - Zone: "sh002", - Addrs: []string{fmt.Sprintf("grpc://127.0.0.%d:9000", i)}, - Metadata: map[string]string{MetaCluster: "c2"}, - } - app.Instances[ins.Zone] = append(app.Instances[ins.Zone], ins) - } - var opt BuildOptions - f := ScheduleNode("sh001") - f.Apply(&opt) - err := compareAddr(opt.Scheduler(app), map[string]int{"sh002": 30, "sh001": 1}) - if err != nil { - t.Fatalf(err.Error()) - } -} - -func Test_Schedule4(t *testing.T) { - app := &InstancesInfo{ - Instances: map[string][]*Instance{}, - Scheduler: &Scheduler{map[string]*ZoneStrategy{"sh001": { - Zones: map[string]*Strategy{ - "sh001": {1}, - "sh002": {30}, - }, - }}}, - } - for i := 0; i < 30; i++ { - ins := &Instance{ - Zone: "sh001", - Addrs: []string{fmt.Sprintf("grpc://127.0.0.%d:9000", i)}, - Metadata: map[string]string{MetaCluster: "c1"}, - } - app.Instances[ins.Zone] = append(app.Instances[ins.Zone], ins) - } - - var opt BuildOptions - f := ScheduleNode("sh001") - f.Apply(&opt) - err := compareAddr(opt.Scheduler(app), map[string]int{"sh001": 30, "sh002": 0}) - if err != nil { - t.Fatalf(err.Error()) - } -} - -func Test_Schedule5(t *testing.T) { - app := &InstancesInfo{ - Instances: map[string][]*Instance{}, - Scheduler: &Scheduler{map[string]*ZoneStrategy{"sh001": { - Zones: map[string]*Strategy{ - "sh002": {30}, - }, - }}}, - } - for i := 0; i < 30; i++ { - ins := &Instance{ - Zone: "sh001", - Addrs: []string{fmt.Sprintf("grpc://127.0.0.%d:9000", i)}, - Metadata: map[string]string{MetaCluster: "c1"}, - } - app.Instances[ins.Zone] = append(app.Instances[ins.Zone], ins) - } - for i := 0; i < 30; i++ { - ins := &Instance{ - Zone: "sh002", - Addrs: []string{fmt.Sprintf("grpc://127.0.0.%d:9000", i)}, - Metadata: map[string]string{MetaCluster: "c2"}, - } - app.Instances[ins.Zone] = append(app.Instances[ins.Zone], ins) - } - var opt BuildOptions - f := ScheduleNode("sh001") - f.Apply(&opt) - err := compareAddr(opt.Scheduler(app), map[string]int{"sh002": 30, "sh001": 0}) - if err != nil { - t.Fatalf(err.Error()) - } -} - -func compareAddr(inss []*Instance, c map[string]int) (err error) { - for _, ins := range inss { - c[ins.Zone] = c[ins.Zone] - 1 - } - for zone, v := range c { - if v != 0 { - err = fmt.Errorf("zone(%s) nums is %d", zone, v) - return - } - } - return -} diff --git a/pkg/naming/zookeeper/zookeeper.go b/pkg/naming/zookeeper/zookeeper.go deleted file mode 100644 index 3b532eb49..000000000 --- a/pkg/naming/zookeeper/zookeeper.go +++ /dev/null @@ -1,396 +0,0 @@ -package zookeeper - -import ( - "context" - "encoding/json" - "errors" - "fmt" - "net/url" - "path" - "strings" - "sync" - "sync/atomic" - "time" - - "github.com/go-zookeeper/zk" - - "github.com/go-kratos/kratos/pkg/log" - "github.com/go-kratos/kratos/pkg/naming" - xtime "github.com/go-kratos/kratos/pkg/time" -) - -// Config is zookeeper config. -type Config struct { - Root string `json:"root"` - Endpoints []string `json:"endpoints"` - Timeout xtime.Duration `json:"timeout"` -} - -var ( - _once sync.Once - _builder naming.Builder - - // ErrDuplication is a register duplication err - ErrDuplication = errors.New("zookeeper: instance duplicate registration") -) - -// Builder return default zookeeper resolver builder. -func Builder(c *Config) naming.Builder { - _once.Do(func() { - _builder, _ = New(c) - }) - return _builder -} - -// Build register resolver into default zookeeper. -func Build(c *Config, id string) naming.Resolver { - return Builder(c).Build(id) -} - -type appInfo struct { - resolver map[*Resolve]struct{} - ins atomic.Value - zkb *Zookeeper - once sync.Once -} - -// Resolve zookeeper resolver. -type Resolve struct { - id string - event chan struct{} - zkb *Zookeeper -} - -// Zookeeper is a zookeeper client Builder. -// path: /{root}/{appid}/{ip} -> json(instance) -type Zookeeper struct { - c *Config - cli *zk.Conn - connEvent <-chan zk.Event - ctx context.Context - cancelFunc context.CancelFunc - - mutex sync.RWMutex - apps map[string]*appInfo - registry map[string]struct{} -} - -// New is new a zookeeper builder. -func New(c *Config) (zkb *Zookeeper, err error) { - if c.Timeout == 0 { - c.Timeout = xtime.Duration(time.Second) - } - if len(c.Endpoints) == 0 { - errInfo := "zookeeper New failed, endpoints is null" - log.Error(errInfo) - return nil, errors.New(errInfo) - } - - zkConn, connEvent, err := zk.Connect(c.Endpoints, time.Duration(c.Timeout)) - if err != nil { - log.Error(fmt.Sprintf("zk Connect err:(%v)", err)) - return - } - log.Info("zk Connect ok!") - - ctx, cancel := context.WithCancel(context.Background()) - zkb = &Zookeeper{ - c: c, - cli: zkConn, - connEvent: connEvent, - ctx: ctx, - cancelFunc: cancel, - apps: map[string]*appInfo{}, - registry: map[string]struct{}{}, - } - return -} - -// Build zookeeper resovler builder. -func (z *Zookeeper) Build(appid string, options ...naming.BuildOpt) naming.Resolver { - r := &Resolve{ - id: appid, - zkb: z, - event: make(chan struct{}, 1), - } - z.mutex.Lock() - app, ok := z.apps[appid] - if !ok { - app = &appInfo{ - resolver: make(map[*Resolve]struct{}), - zkb: z, - } - z.apps[appid] = app - } - app.resolver[r] = struct{}{} - z.mutex.Unlock() - if ok { - select { - case r.event <- struct{}{}: - default: - } - } - app.once.Do(func() { - go app.watch(appid) - }) - return r -} - -// Scheme return zookeeper's scheme. -func (z *Zookeeper) Scheme() string { - return "zookeeper" -} - -// Register is register instance. -func (z *Zookeeper) Register(ctx context.Context, ins *naming.Instance) (cancelFunc context.CancelFunc, err error) { - z.mutex.Lock() - if _, ok := z.registry[ins.AppID]; ok { - err = ErrDuplication - } else { - z.registry[ins.AppID] = struct{}{} - } - z.mutex.Unlock() - if err != nil { - return - } - ctx, cancel := context.WithCancel(z.ctx) - if err = z.register(ctx, ins); err != nil { - z.mutex.Lock() - delete(z.registry, ins.AppID) - z.mutex.Unlock() - cancel() - return - } - ch := make(chan struct{}, 1) - cancelFunc = context.CancelFunc(func() { - cancel() - <-ch - }) - go func() { - for { - select { - case connEvent := <-z.connEvent: - log.Info("watch zkClient state, connEvent:(%+v)", connEvent) - if connEvent.State == zk.StateHasSession { - if err = z.register(ctx, ins); err != nil { - log.Warn(fmt.Sprintf("watch zkClient state, fail to register node error:(%v)", err)) - continue - } - } - case <-ctx.Done(): - ch <- struct{}{} - return - } - } - }() - return -} - -func (z *Zookeeper) createPath(paths string) error { - var ( - lastPath = "/" - seps = strings.Split(paths, "/") - ) - for _, part := range seps { - if part == "" { - continue - } - lastPath = path.Join(lastPath, part) - ok, _, err := z.cli.Exists(lastPath) - if err != nil { - return err - } - if ok { - continue - } - ret, err := z.cli.Create(lastPath, nil, 0, zk.WorldACL(zk.PermAll)) - if err != nil { - log.Warn(fmt.Sprintf("createPath, fail to Create node:(%s). error:(%v)", paths, err)) - } else { - log.Info(fmt.Sprintf("createPath, succeed to Create node:(%s). retStr:(%s)", paths, ret)) - } - } - return nil -} - -func (z *Zookeeper) registerPeerServer(nodePath string, ins *naming.Instance) (err error) { - var ( - str string - ) - val, err := json.Marshal(ins) - if err != nil { - return - } - log.Info(fmt.Sprintf("registerPeerServer, ins after json.Marshal:(%v)", string(val))) - ok, _, err := z.cli.Exists(nodePath) - if err != nil { - return err - } - if ok { - return nil - } - str, err = z.cli.Create(nodePath, val, zk.FlagEphemeral, zk.WorldACL(zk.PermAll)) - if err != nil { - log.Warn(fmt.Sprintf("registerPeerServer, fail to Create node:%s. error:(%v)", nodePath, err)) - } else { - log.Info(fmt.Sprintf("registerPeerServer, succeed to Create node:%s. retStr:(%s)", nodePath, str)) - } - return -} - -// register is register instance to zookeeper. -func (z *Zookeeper) register(ctx context.Context, ins *naming.Instance) (err error) { - log.Info("zookeeper register enter, instance Addrs:(%v)", ins.Addrs) - - prefix := z.keyPrefix(ins.AppID) - if err = z.createPath(prefix); err != nil { - log.Warn(fmt.Sprintf("register, fail to createPath node error:(%v)", err)) - } - for _, addr := range ins.Addrs { - u, err := url.Parse(addr) - if err != nil { - continue - } - // grpc://127.0.0.1:8000 to 127.0.0.1 - nodePath := prefix + "/" + strings.SplitN(u.Host, ":", 2)[0] - if err = z.registerPeerServer(nodePath, ins); err != nil { - log.Warn(fmt.Sprintf("registerServer, fail to RegisterPeerServer node:%s error:(%v)", addr, err)) - } else { - log.Info("registerServer, succeed to RegistServer node.") - } - } - return nil -} - -func (z *Zookeeper) unregister(ins *naming.Instance) (err error) { - log.Info("zookeeper unregister enter, instance Addrs:(%v)", ins.Addrs) - prefix := z.keyPrefix(ins.AppID) - for _, addr := range ins.Addrs { - u, err := url.Parse(addr) - if err != nil { - continue - } - // grpc://127.0.0.1:8000 to 127.0.0.1 - nodePath := prefix + "/" + strings.SplitN(u.Host, ":", 2)[0] - exists, _, err := z.cli.Exists(nodePath) - if err != nil { - log.Error("zk.Conn.Exists node:(%v), error:(%v)", nodePath, err) - continue - } - if exists { - _, s, err := z.cli.Get(nodePath) - if err != nil { - log.Error("zk.Conn.Get node:(%s), error:(%v)", nodePath, err) - continue - } - if err = z.cli.Delete(nodePath, s.Version); err != nil { - log.Error("zk.Conn.Delete node:(%s), error:(%v)", nodePath, err) - continue - } - } - - log.Info(fmt.Sprintf("unregister, client.Delete:(%v), appid:(%v), hostname:(%v) success", nodePath, ins.AppID, ins.Hostname)) - } - return -} - -func (z *Zookeeper) keyPrefix(appID string) string { - return path.Join(z.c.Root, appID) -} - -// Close stop all running process including zk fetch and register. -func (z *Zookeeper) Close() error { - z.cancelFunc() - return nil -} - -func (a *appInfo) watch(appID string) { - _ = a.fetchstore(appID) - go func() { - prefix := a.zkb.keyPrefix(appID) - for { - log.Info(fmt.Sprintf("zk ChildrenW enter, prefix:(%v)", prefix)) - snapshot, _, event, err := a.zkb.cli.ChildrenW(prefix) - if err != nil { - log.Error("zk ChildrenW fail to watch:%s error:(%v)", prefix, err) - time.Sleep(time.Second) - _ = a.fetchstore(appID) - continue - } - log.Info(fmt.Sprintf("zk ChildrenW ok, prefix:%s snapshot:(%v)", prefix, snapshot)) - for ev := range event { - log.Info(fmt.Sprintf("zk ChildrenW ok, prefix:(%v), event Path:(%v), Type:(%v)", prefix, ev.Path, ev.Type)) - if ev.Type == zk.EventNodeChildrenChanged { - _ = a.fetchstore(appID) - } - } - } - }() -} - -func (a *appInfo) fetchstore(appID string) (err error) { - prefix := a.zkb.keyPrefix(appID) - childs, _, err := a.zkb.cli.Children(prefix) - if err != nil { - log.Error(fmt.Sprintf("fetchstore, fail to get Children of node:(%v), error:(%v)", prefix, err)) - return - } - log.Info(fmt.Sprintf("fetchstore, ok to get Children of node:(%v), childs:(%v)", prefix, childs)) - ins := &naming.InstancesInfo{ - Instances: make(map[string][]*naming.Instance), - } - for _, child := range childs { - nodePath := prefix + "/" + child - resp, _, err := a.zkb.cli.Get(nodePath) - if err != nil { - log.Error("zookeeper: fetch client.Get(%s) error:(%v)", nodePath, err) - return err - } - in := new(naming.Instance) - if err = json.Unmarshal(resp, in); err != nil { - return err - } - ins.Instances[in.Zone] = append(ins.Instances[in.Zone], in) - } - a.store(ins) - return nil -} - -func (a *appInfo) store(ins *naming.InstancesInfo) { - a.ins.Store(ins) - a.zkb.mutex.RLock() - for rs := range a.resolver { - select { - case rs.event <- struct{}{}: - default: - } - } - a.zkb.mutex.RUnlock() -} - -// Watch watch instance. -func (r *Resolve) Watch() <-chan struct{} { - return r.event -} - -// Fetch fetch resolver instance. -func (r *Resolve) Fetch(ctx context.Context) (ins *naming.InstancesInfo, ok bool) { - r.zkb.mutex.RLock() - app, ok := r.zkb.apps[r.id] - r.zkb.mutex.RUnlock() - if ok { - ins, ok = app.ins.Load().(*naming.InstancesInfo) - return - } - return -} - -// Close close resolver. -func (r *Resolve) Close() error { - r.zkb.mutex.Lock() - if app, ok := r.zkb.apps[r.id]; ok && len(app.resolver) != 0 { - delete(app.resolver, r) - } - r.zkb.mutex.Unlock() - return nil -} diff --git a/pkg/net/criticality/criticality.go b/pkg/net/criticality/criticality.go deleted file mode 100644 index 9e76e4891..000000000 --- a/pkg/net/criticality/criticality.go +++ /dev/null @@ -1,57 +0,0 @@ -package criticality - -// Criticality is -type Criticality string - -// criticality -var ( - // EmptyCriticality is used to mark any invalid criticality, and the empty criticality will be parsed as the default criticality later. - EmptyCriticality = Criticality("") - // CriticalPlus is reserved for the most critical requests, those that will result in serious user-visible impact if they fail. - CriticalPlus = Criticality("CRITICAL_PLUS") - // Critical is the default value for requests sent from production jobs. These requests will result in user-visible impact, but the impact may be less severe than those of CRITICAL_PLUS. Services are expected to provision enough capacity for all expected CRITICAL and CRITICAL_PLUS traffic. - Critical = Criticality("CRITICAL") - // SheddablePlus is traffic for which partial unavailability is expected. This is the default for batch jobs, which can retry requests minutes or even hours later. - SheddablePlus = Criticality("SHEDDABLE_PLUS") - // Sheddable is traffic for which frequent partial unavailability and occasional full unavailability is expected. - Sheddable = Criticality("SHEDDABLE") - - // higher is more critical - _criticalityEnum = map[Criticality]int{ - CriticalPlus: 40, - Critical: 30, - SheddablePlus: 20, - Sheddable: 10, - } - - _defaultCriticality = Critical -) - -// Value is used to get criticality value, higher value is more critical. -func Value(in Criticality) int { - v, ok := _criticalityEnum[in] - if !ok { - return _criticalityEnum[_defaultCriticality] - } - return v -} - -// Higher will compare the input criticality with self, return true if the input is more critical than self. -func (c Criticality) Higher(in Criticality) bool { - return Value(in) > Value(c) -} - -// Parse will parse raw criticality string as valid critality. Any invalid input will parse as empty criticality. -func Parse(raw string) Criticality { - crtl := Criticality(raw) - if _, ok := _criticalityEnum[crtl]; ok { - return crtl - } - return EmptyCriticality -} - -// Exist is used to check criticality is exist in several enumeration. -func Exist(c Criticality) bool { - _, ok := _criticalityEnum[c] - return ok -} diff --git a/pkg/net/http/blademaster/README.md b/pkg/net/http/blademaster/README.md deleted file mode 100644 index 7029187d2..000000000 --- a/pkg/net/http/blademaster/README.md +++ /dev/null @@ -1,5 +0,0 @@ -#### net/http/blademaster - -##### 项目简介 - -http 框架,带来如飞一般的体验。 diff --git a/pkg/net/http/blademaster/binding/binding.go b/pkg/net/http/blademaster/binding/binding.go deleted file mode 100644 index ea2c8e017..000000000 --- a/pkg/net/http/blademaster/binding/binding.go +++ /dev/null @@ -1,88 +0,0 @@ -package binding - -import ( - "net/http" - "strings" - - "gopkg.in/go-playground/validator.v9" -) - -// MIME -const ( - MIMEJSON = "application/json" - MIMEHTML = "text/html" - MIMEXML = "application/xml" - MIMEXML2 = "text/xml" - MIMEPlain = "text/plain" - MIMEPOSTForm = "application/x-www-form-urlencoded" - MIMEMultipartPOSTForm = "multipart/form-data" -) - -// Binding http binding request interface. -type Binding interface { - Name() string - Bind(*http.Request, interface{}) error -} - -// StructValidator http validator interface. -type StructValidator interface { - // ValidateStruct can receive any kind of type and it should never panic, even if the configuration is not right. - // If the received type is not a struct, any validation should be skipped and nil must be returned. - // If the received type is a struct or pointer to a struct, the validation should be performed. - // If the struct is not valid or the validation itself fails, a descriptive error should be returned. - // Otherwise nil must be returned. - ValidateStruct(interface{}) error - - // RegisterValidation adds a validation Func to a Validate's map of validators denoted by the key - // NOTE: if the key already exists, the previous validation function will be replaced. - // NOTE: this method is not thread-safe it is intended that these all be registered prior to any validation - RegisterValidation(string, validator.Func) error - - //GetValidate return the default validate - GetValidate() *validator.Validate -} - -// Validator default validator. -var Validator StructValidator = &defaultValidator{} - -// Binding -var ( - JSON = jsonBinding{} - XML = xmlBinding{} - Form = formBinding{} - Query = queryBinding{} - FormPost = formPostBinding{} - FormMultipart = formMultipartBinding{} -) - -// Default get by binding type by method and contexttype. -func Default(method, contentType string) Binding { - if method == "GET" { - return Form - } - - contentType = stripContentTypeParam(contentType) - switch contentType { - case MIMEJSON: - return JSON - case MIMEXML, MIMEXML2: - return XML - default: //case MIMEPOSTForm, MIMEMultipartPOSTForm: - return Form - } -} - -func validate(obj interface{}) error { - if Validator == nil { - return nil - } - return Validator.ValidateStruct(obj) -} - -func stripContentTypeParam(contentType string) string { - i := strings.Index(contentType, ";") - if i != -1 { - contentType = contentType[:i] - } - return contentType -} diff --git a/pkg/net/http/blademaster/binding/binding_test.go b/pkg/net/http/blademaster/binding/binding_test.go deleted file mode 100644 index 9b84c6c2f..000000000 --- a/pkg/net/http/blademaster/binding/binding_test.go +++ /dev/null @@ -1,341 +0,0 @@ -package binding - -import ( - "bytes" - "mime/multipart" - "net/http" - "testing" - - "github.com/stretchr/testify/assert" -) - -type FooStruct struct { - Foo string `msgpack:"foo" json:"foo" form:"foo" xml:"foo" validate:"required"` -} - -type FooBarStruct struct { - FooStruct - Bar string `msgpack:"bar" json:"bar" form:"bar" xml:"bar" validate:"required"` - Slice []string `form:"slice" validate:"max=10"` -} - -type ComplexDefaultStruct struct { - Int int `form:"int" default:"999"` - String string `form:"string" default:"default-string"` - Bool bool `form:"bool" default:"false"` - Int64Slice []int64 `form:"int64_slice,split" default:"1,2,3,4"` - Int8Slice []int8 `form:"int8_slice,split" default:"1,2,3,4"` -} - -type Int8SliceStruct struct { - State []int8 `form:"state,split"` -} - -type Int64SliceStruct struct { - State []int64 `form:"state,split"` -} - -type StringSliceStruct struct { - State []string `form:"state,split"` -} - -func TestBindingDefault(t *testing.T) { - assert.Equal(t, Default("GET", ""), Form) - assert.Equal(t, Default("GET", MIMEJSON), Form) - assert.Equal(t, Default("GET", MIMEJSON+"; charset=utf-8"), Form) - - assert.Equal(t, Default("POST", MIMEJSON), JSON) - assert.Equal(t, Default("PUT", MIMEJSON), JSON) - - assert.Equal(t, Default("POST", MIMEJSON+"; charset=utf-8"), JSON) - assert.Equal(t, Default("PUT", MIMEJSON+"; charset=utf-8"), JSON) - - assert.Equal(t, Default("POST", MIMEXML), XML) - assert.Equal(t, Default("PUT", MIMEXML2), XML) - - assert.Equal(t, Default("POST", MIMEPOSTForm), Form) - assert.Equal(t, Default("PUT", MIMEPOSTForm), Form) - - assert.Equal(t, Default("POST", MIMEPOSTForm+"; charset=utf-8"), Form) - assert.Equal(t, Default("PUT", MIMEPOSTForm+"; charset=utf-8"), Form) - - assert.Equal(t, Default("POST", MIMEMultipartPOSTForm), Form) - assert.Equal(t, Default("PUT", MIMEMultipartPOSTForm), Form) -} - -func TestStripContentType(t *testing.T) { - c1 := "application/vnd.mozilla.xul+xml" - c2 := "application/vnd.mozilla.xul+xml; charset=utf-8" - assert.Equal(t, stripContentTypeParam(c1), c1) - assert.Equal(t, stripContentTypeParam(c2), "application/vnd.mozilla.xul+xml") -} - -func TestBindInt8Form(t *testing.T) { - params := "state=1,2,3" - req, _ := http.NewRequest("GET", "http://api.bilibili.com/test?"+params, nil) - q := new(Int8SliceStruct) - Form.Bind(req, q) - assert.EqualValues(t, []int8{1, 2, 3}, q.State) - - params = "state=1,2,3,256" - req, _ = http.NewRequest("GET", "http://api.bilibili.com/test?"+params, nil) - q = new(Int8SliceStruct) - assert.Error(t, Form.Bind(req, q)) - - params = "state=" - req, _ = http.NewRequest("GET", "http://api.bilibili.com/test?"+params, nil) - q = new(Int8SliceStruct) - assert.NoError(t, Form.Bind(req, q)) - assert.Len(t, q.State, 0) - - params = "state=1,,2" - req, _ = http.NewRequest("GET", "http://api.bilibili.com/test?"+params, nil) - q = new(Int8SliceStruct) - assert.NoError(t, Form.Bind(req, q)) - assert.EqualValues(t, []int8{1, 2}, q.State) -} - -func TestBindInt64Form(t *testing.T) { - params := "state=1,2,3" - req, _ := http.NewRequest("GET", "http://api.bilibili.com/test?"+params, nil) - q := new(Int64SliceStruct) - Form.Bind(req, q) - assert.EqualValues(t, []int64{1, 2, 3}, q.State) - - params = "state=" - req, _ = http.NewRequest("GET", "http://api.bilibili.com/test?"+params, nil) - q = new(Int64SliceStruct) - assert.NoError(t, Form.Bind(req, q)) - assert.Len(t, q.State, 0) -} - -func TestBindStringForm(t *testing.T) { - params := "state=1,2,3" - req, _ := http.NewRequest("GET", "http://api.bilibili.com/test?"+params, nil) - q := new(StringSliceStruct) - Form.Bind(req, q) - assert.EqualValues(t, []string{"1", "2", "3"}, q.State) - - params = "state=" - req, _ = http.NewRequest("GET", "http://api.bilibili.com/test?"+params, nil) - q = new(StringSliceStruct) - assert.NoError(t, Form.Bind(req, q)) - assert.Len(t, q.State, 0) - - params = "state=p,,p" - req, _ = http.NewRequest("GET", "http://api.bilibili.com/test?"+params, nil) - q = new(StringSliceStruct) - Form.Bind(req, q) - assert.EqualValues(t, []string{"p", "p"}, q.State) -} - -func TestBindingJSON(t *testing.T) { - testBodyBinding(t, - JSON, "json", - "/", "/", - `{"foo": "bar"}`, `{"bar": "foo"}`) -} - -func TestBindingForm(t *testing.T) { - testFormBinding(t, "POST", - "/", "/", - "foo=bar&bar=foo&slice=a&slice=b", "bar2=foo") -} - -func TestBindingForm2(t *testing.T) { - testFormBinding(t, "GET", - "/?foo=bar&bar=foo", "/?bar2=foo", - "", "") -} - -func TestBindingQuery(t *testing.T) { - testQueryBinding(t, "POST", - "/?foo=bar&bar=foo", "/", - "foo=unused", "bar2=foo") -} - -func TestBindingQuery2(t *testing.T) { - testQueryBinding(t, "GET", - "/?foo=bar&bar=foo", "/?bar2=foo", - "foo=unused", "") -} - -func TestBindingXML(t *testing.T) { - testBodyBinding(t, - XML, "xml", - "/", "/", - "bar", "foo") -} - -func createFormPostRequest() *http.Request { - req, _ := http.NewRequest("POST", "/?foo=getfoo&bar=getbar", bytes.NewBufferString("foo=bar&bar=foo")) - req.Header.Set("Content-Type", MIMEPOSTForm) - return req -} - -func createFormMultipartRequest() *http.Request { - boundary := "--testboundary" - body := new(bytes.Buffer) - mw := multipart.NewWriter(body) - defer mw.Close() - - mw.SetBoundary(boundary) - mw.WriteField("foo", "bar") - mw.WriteField("bar", "foo") - req, _ := http.NewRequest("POST", "/?foo=getfoo&bar=getbar", body) - req.Header.Set("Content-Type", MIMEMultipartPOSTForm+"; boundary="+boundary) - return req -} - -func TestBindingFormPost(t *testing.T) { - req := createFormPostRequest() - var obj FooBarStruct - FormPost.Bind(req, &obj) - - assert.Equal(t, obj.Foo, "bar") - assert.Equal(t, obj.Bar, "foo") -} - -func TestBindingFormMultipart(t *testing.T) { - req := createFormMultipartRequest() - var obj FooBarStruct - FormMultipart.Bind(req, &obj) - - assert.Equal(t, obj.Foo, "bar") - assert.Equal(t, obj.Bar, "foo") -} - -func TestValidationFails(t *testing.T) { - var obj FooStruct - req := requestWithBody("POST", "/", `{"bar": "foo"}`) - err := JSON.Bind(req, &obj) - assert.Error(t, err) -} - -func TestValidationDisabled(t *testing.T) { - backup := Validator - Validator = nil - defer func() { Validator = backup }() - - var obj FooStruct - req := requestWithBody("POST", "/", `{"bar": "foo"}`) - err := JSON.Bind(req, &obj) - assert.NoError(t, err) -} - -func TestExistsSucceeds(t *testing.T) { - type HogeStruct struct { - Hoge *int `json:"hoge" binding:"exists"` - } - - var obj HogeStruct - req := requestWithBody("POST", "/", `{"hoge": 0}`) - err := JSON.Bind(req, &obj) - assert.NoError(t, err) -} - -func TestFormDefaultValue(t *testing.T) { - params := "int=333&string=hello&bool=true&int64_slice=5,6,7,8&int8_slice=5,6,7,8" - req, _ := http.NewRequest("GET", "http://api.bilibili.com/test?"+params, nil) - q := new(ComplexDefaultStruct) - assert.NoError(t, Form.Bind(req, q)) - assert.Equal(t, 333, q.Int) - assert.Equal(t, "hello", q.String) - assert.Equal(t, true, q.Bool) - assert.EqualValues(t, []int64{5, 6, 7, 8}, q.Int64Slice) - assert.EqualValues(t, []int8{5, 6, 7, 8}, q.Int8Slice) - - params = "string=hello&bool=false" - req, _ = http.NewRequest("GET", "http://api.bilibili.com/test?"+params, nil) - q = new(ComplexDefaultStruct) - assert.NoError(t, Form.Bind(req, q)) - assert.Equal(t, 999, q.Int) - assert.Equal(t, "hello", q.String) - assert.Equal(t, false, q.Bool) - assert.EqualValues(t, []int64{1, 2, 3, 4}, q.Int64Slice) - assert.EqualValues(t, []int8{1, 2, 3, 4}, q.Int8Slice) - - params = "strings=hello" - req, _ = http.NewRequest("GET", "http://api.bilibili.com/test?"+params, nil) - q = new(ComplexDefaultStruct) - assert.NoError(t, Form.Bind(req, q)) - assert.Equal(t, 999, q.Int) - assert.Equal(t, "default-string", q.String) - assert.Equal(t, false, q.Bool) - assert.EqualValues(t, []int64{1, 2, 3, 4}, q.Int64Slice) - assert.EqualValues(t, []int8{1, 2, 3, 4}, q.Int8Slice) - - params = "int=&string=&bool=true&int64_slice=&int8_slice=" - req, _ = http.NewRequest("GET", "http://api.bilibili.com/test?"+params, nil) - q = new(ComplexDefaultStruct) - assert.NoError(t, Form.Bind(req, q)) - assert.Equal(t, 999, q.Int) - assert.Equal(t, "default-string", q.String) - assert.Equal(t, true, q.Bool) - assert.EqualValues(t, []int64{1, 2, 3, 4}, q.Int64Slice) - assert.EqualValues(t, []int8{1, 2, 3, 4}, q.Int8Slice) -} - -func testFormBinding(t *testing.T, method, path, badPath, body, badBody string) { - b := Form - assert.Equal(t, b.Name(), "form") - - obj := FooBarStruct{} - req := requestWithBody(method, path, body) - if method == "POST" { - req.Header.Add("Content-Type", MIMEPOSTForm) - } - err := b.Bind(req, &obj) - assert.NoError(t, err) - assert.Equal(t, obj.Foo, "bar") - assert.Equal(t, obj.Bar, "foo") - - obj = FooBarStruct{} - req = requestWithBody(method, badPath, badBody) - err = JSON.Bind(req, &obj) - assert.Error(t, err) -} - -func testQueryBinding(t *testing.T, method, path, badPath, body, badBody string) { - b := Query - assert.Equal(t, b.Name(), "query") - - obj := FooBarStruct{} - req := requestWithBody(method, path, body) - if method == "POST" { - req.Header.Add("Content-Type", MIMEPOSTForm) - } - err := b.Bind(req, &obj) - assert.NoError(t, err) - assert.Equal(t, obj.Foo, "bar") - assert.Equal(t, obj.Bar, "foo") -} - -func testBodyBinding(t *testing.T, b Binding, name, path, badPath, body, badBody string) { - assert.Equal(t, b.Name(), name) - - obj := FooStruct{} - req := requestWithBody("POST", path, body) - err := b.Bind(req, &obj) - assert.NoError(t, err) - assert.Equal(t, obj.Foo, "bar") - - obj = FooStruct{} - req = requestWithBody("POST", badPath, badBody) - err = JSON.Bind(req, &obj) - assert.Error(t, err) -} - -func requestWithBody(method, path, body string) (req *http.Request) { - req, _ = http.NewRequest(method, path, bytes.NewBufferString(body)) - return -} -func BenchmarkBindingForm(b *testing.B) { - req := requestWithBody("POST", "/", "foo=bar&bar=foo&slice=a&slice=b&slice=c&slice=w") - req.Header.Add("Content-Type", MIMEPOSTForm) - f := Form - for i := 0; i < b.N; i++ { - obj := FooBarStruct{} - f.Bind(req, &obj) - } -} diff --git a/pkg/net/http/blademaster/binding/default_validator.go b/pkg/net/http/blademaster/binding/default_validator.go deleted file mode 100644 index cced3f3e1..000000000 --- a/pkg/net/http/blademaster/binding/default_validator.go +++ /dev/null @@ -1,50 +0,0 @@ -package binding - -import ( - "reflect" - "sync" - - "gopkg.in/go-playground/validator.v9" -) - -type defaultValidator struct { - once sync.Once - validate *validator.Validate -} - -var _ StructValidator = &defaultValidator{} - -func (v *defaultValidator) ValidateStruct(obj interface{}) error { - if kindOfData(obj) == reflect.Struct { - v.lazyinit() - if err := v.validate.Struct(obj); err != nil { - return err - } - } - return nil -} - -func (v *defaultValidator) RegisterValidation(key string, fn validator.Func) error { - v.lazyinit() - return v.validate.RegisterValidation(key, fn) -} - -func (v *defaultValidator) lazyinit() { - v.once.Do(func() { - v.validate = validator.New() - }) -} - -func kindOfData(data interface{}) reflect.Kind { - value := reflect.ValueOf(data) - valueType := value.Kind() - if valueType == reflect.Ptr { - valueType = value.Elem().Kind() - } - return valueType -} - -func (v *defaultValidator) GetValidate() *validator.Validate { - v.lazyinit() - return v.validate -} diff --git a/pkg/net/http/blademaster/binding/example/test.pb.go b/pkg/net/http/blademaster/binding/example/test.pb.go deleted file mode 100644 index 3de8444ff..000000000 --- a/pkg/net/http/blademaster/binding/example/test.pb.go +++ /dev/null @@ -1,113 +0,0 @@ -// Code generated by protoc-gen-go. -// source: test.proto -// DO NOT EDIT! - -/* -Package example is a generated protocol buffer package. - -It is generated from these files: - test.proto - -It has these top-level messages: - Test -*/ -package example - -import proto "github.com/golang/protobuf/proto" -import math "math" - -// Reference imports to suppress errors if they are not otherwise used. -var _ = proto.Marshal -var _ = math.Inf - -type FOO int32 - -const ( - FOO_X FOO = 17 -) - -var FOO_name = map[int32]string{ - 17: "X", -} -var FOO_value = map[string]int32{ - "X": 17, -} - -func (x FOO) Enum() *FOO { - p := new(FOO) - *p = x - return p -} -func (x FOO) String() string { - return proto.EnumName(FOO_name, int32(x)) -} -func (x *FOO) UnmarshalJSON(data []byte) error { - value, err := proto.UnmarshalJSONEnum(FOO_value, data, "FOO") - if err != nil { - return err - } - *x = FOO(value) - return nil -} - -type Test struct { - Label *string `protobuf:"bytes,1,req,name=label" json:"label,omitempty"` - Type *int32 `protobuf:"varint,2,opt,name=type,def=77" json:"type,omitempty"` - Reps []int64 `protobuf:"varint,3,rep,name=reps" json:"reps,omitempty"` - Optionalgroup *Test_OptionalGroup `protobuf:"group,4,opt,name=OptionalGroup" json:"optionalgroup,omitempty"` - XXX_unrecognized []byte `json:"-"` -} - -func (m *Test) Reset() { *m = Test{} } -func (m *Test) String() string { return proto.CompactTextString(m) } -func (*Test) ProtoMessage() {} - -const Default_Test_Type int32 = 77 - -func (m *Test) GetLabel() string { - if m != nil && m.Label != nil { - return *m.Label - } - return "" -} - -func (m *Test) GetType() int32 { - if m != nil && m.Type != nil { - return *m.Type - } - return Default_Test_Type -} - -func (m *Test) GetReps() []int64 { - if m != nil { - return m.Reps - } - return nil -} - -func (m *Test) GetOptionalgroup() *Test_OptionalGroup { - if m != nil { - return m.Optionalgroup - } - return nil -} - -type Test_OptionalGroup struct { - RequiredField *string `protobuf:"bytes,5,req" json:"RequiredField,omitempty"` - XXX_unrecognized []byte `json:"-"` -} - -func (m *Test_OptionalGroup) Reset() { *m = Test_OptionalGroup{} } -func (m *Test_OptionalGroup) String() string { return proto.CompactTextString(m) } -func (*Test_OptionalGroup) ProtoMessage() {} - -func (m *Test_OptionalGroup) GetRequiredField() string { - if m != nil && m.RequiredField != nil { - return *m.RequiredField - } - return "" -} - -func init() { - proto.RegisterEnum("example.FOO", FOO_name, FOO_value) -} diff --git a/pkg/net/http/blademaster/binding/example/test.proto b/pkg/net/http/blademaster/binding/example/test.proto deleted file mode 100644 index 8ee9800aa..000000000 --- a/pkg/net/http/blademaster/binding/example/test.proto +++ /dev/null @@ -1,12 +0,0 @@ -package example; - -enum FOO {X=17;}; - -message Test { - required string label = 1; - optional int32 type = 2[default=77]; - repeated int64 reps = 3; - optional group OptionalGroup = 4{ - required string RequiredField = 5; - } -} diff --git a/pkg/net/http/blademaster/binding/example_test.go b/pkg/net/http/blademaster/binding/example_test.go deleted file mode 100644 index c667a517c..000000000 --- a/pkg/net/http/blademaster/binding/example_test.go +++ /dev/null @@ -1,36 +0,0 @@ -package binding - -import ( - "fmt" - "log" - "net/http" -) - -type Arg struct { - Max int64 `form:"max" validate:"max=10"` - Min int64 `form:"min" validate:"min=2"` - Range int64 `form:"range" validate:"min=1,max=10"` - // use split option to split arg 1,2,3 into slice [1 2 3] - // otherwise slice type with parse url.Values (eg:a=b&a=c) default. - Slice []int64 `form:"slice,split" validate:"min=1"` -} - -func ExampleBinding() { - req := initHTTP("max=9&min=3&range=3&slice=1,2,3") - arg := new(Arg) - if err := Form.Bind(req, arg); err != nil { - log.Fatal(err) - } - fmt.Printf("arg.Max %d\narg.Min %d\narg.Range %d\narg.Slice %v", arg.Max, arg.Min, arg.Range, arg.Slice) - // Output: - // arg.Max 9 - // arg.Min 3 - // arg.Range 3 - // arg.Slice [1 2 3] -} - -func initHTTP(params string) (req *http.Request) { - req, _ = http.NewRequest("GET", "http://api.bilibili.com/test?"+params, nil) - req.ParseForm() - return -} diff --git a/pkg/net/http/blademaster/binding/form.go b/pkg/net/http/blademaster/binding/form.go deleted file mode 100644 index 61aa5ee83..000000000 --- a/pkg/net/http/blademaster/binding/form.go +++ /dev/null @@ -1,55 +0,0 @@ -package binding - -import ( - "net/http" - - "github.com/pkg/errors" -) - -const defaultMemory = 32 * 1024 * 1024 - -type formBinding struct{} -type formPostBinding struct{} -type formMultipartBinding struct{} - -func (f formBinding) Name() string { - return "form" -} - -func (f formBinding) Bind(req *http.Request, obj interface{}) error { - if err := req.ParseForm(); err != nil { - return errors.WithStack(err) - } - if err := mapForm(obj, req.Form); err != nil { - return err - } - return validate(obj) -} - -func (f formPostBinding) Name() string { - return "form-urlencoded" -} - -func (f formPostBinding) Bind(req *http.Request, obj interface{}) error { - if err := req.ParseForm(); err != nil { - return errors.WithStack(err) - } - if err := mapForm(obj, req.PostForm); err != nil { - return err - } - return validate(obj) -} - -func (f formMultipartBinding) Name() string { - return "multipart/form-data" -} - -func (f formMultipartBinding) Bind(req *http.Request, obj interface{}) error { - if err := req.ParseMultipartForm(defaultMemory); err != nil { - return errors.WithStack(err) - } - if err := mapForm(obj, req.MultipartForm.Value); err != nil { - return err - } - return validate(obj) -} diff --git a/pkg/net/http/blademaster/binding/form_mapping.go b/pkg/net/http/blademaster/binding/form_mapping.go deleted file mode 100644 index ac4ecd116..000000000 --- a/pkg/net/http/blademaster/binding/form_mapping.go +++ /dev/null @@ -1,276 +0,0 @@ -package binding - -import ( - "reflect" - "strconv" - "strings" - "sync" - "time" - - "github.com/pkg/errors" -) - -// scache struct reflect type cache. -var scache = &cache{ - data: make(map[reflect.Type]*sinfo), -} - -type cache struct { - data map[reflect.Type]*sinfo - mutex sync.RWMutex -} - -func (c *cache) get(obj reflect.Type) (s *sinfo) { - var ok bool - c.mutex.RLock() - if s, ok = c.data[obj]; !ok { - c.mutex.RUnlock() - s = c.set(obj) - return - } - c.mutex.RUnlock() - return -} - -func (c *cache) set(obj reflect.Type) (s *sinfo) { - s = new(sinfo) - tp := obj.Elem() - for i := 0; i < tp.NumField(); i++ { - fd := new(field) - fd.tp = tp.Field(i) - tag := fd.tp.Tag.Get("form") - fd.name, fd.option = parseTag(tag) - if defV := fd.tp.Tag.Get("default"); defV != "" { - dv := reflect.New(fd.tp.Type).Elem() - setWithProperType(fd.tp.Type.Kind(), []string{defV}, dv, fd.option) - fd.hasDefault = true - fd.defaultValue = dv - } - s.field = append(s.field, fd) - } - c.mutex.Lock() - c.data[obj] = s - c.mutex.Unlock() - return -} - -type sinfo struct { - field []*field -} - -type field struct { - tp reflect.StructField - name string - option tagOptions - - hasDefault bool // if field had default value - defaultValue reflect.Value // field default value -} - -func mapForm(ptr interface{}, form map[string][]string) error { - sinfo := scache.get(reflect.TypeOf(ptr)) - val := reflect.ValueOf(ptr).Elem() - for i, fd := range sinfo.field { - typeField := fd.tp - structField := val.Field(i) - if !structField.CanSet() { - continue - } - - structFieldKind := structField.Kind() - inputFieldName := fd.name - if inputFieldName == "" { - inputFieldName = typeField.Name - - // if "form" tag is nil, we inspect if the field is a struct. - // this would not make sense for JSON parsing but it does for a form - // since data is flatten - if structFieldKind == reflect.Struct { - err := mapForm(structField.Addr().Interface(), form) - if err != nil { - return err - } - continue - } - } - inputValue, exists := form[inputFieldName] - if !exists { - // Set the field as default value when the input value is not exist - if fd.hasDefault { - structField.Set(fd.defaultValue) - } - continue - } - // Set the field as default value when the input value is empty - if fd.hasDefault && inputValue[0] == "" { - structField.Set(fd.defaultValue) - continue - } - if _, isTime := structField.Interface().(time.Time); isTime { - if err := setTimeField(inputValue[0], typeField, structField); err != nil { - return err - } - continue - } - if err := setWithProperType(typeField.Type.Kind(), inputValue, structField, fd.option); err != nil { - return err - } - } - return nil -} - -func setWithProperType(valueKind reflect.Kind, val []string, structField reflect.Value, option tagOptions) error { - switch valueKind { - case reflect.Int: - return setIntField(val[0], 0, structField) - case reflect.Int8: - return setIntField(val[0], 8, structField) - case reflect.Int16: - return setIntField(val[0], 16, structField) - case reflect.Int32: - return setIntField(val[0], 32, structField) - case reflect.Int64: - return setIntField(val[0], 64, structField) - case reflect.Uint: - return setUintField(val[0], 0, structField) - case reflect.Uint8: - return setUintField(val[0], 8, structField) - case reflect.Uint16: - return setUintField(val[0], 16, structField) - case reflect.Uint32: - return setUintField(val[0], 32, structField) - case reflect.Uint64: - return setUintField(val[0], 64, structField) - case reflect.Bool: - return setBoolField(val[0], structField) - case reflect.Float32: - return setFloatField(val[0], 32, structField) - case reflect.Float64: - return setFloatField(val[0], 64, structField) - case reflect.String: - structField.SetString(val[0]) - case reflect.Slice: - if option.Contains("split") { - val = strings.Split(val[0], ",") - } - filtered := filterEmpty(val) - switch structField.Type().Elem().Kind() { - case reflect.Int64: - valSli := make([]int64, 0, len(filtered)) - for i := 0; i < len(filtered); i++ { - d, err := strconv.ParseInt(filtered[i], 10, 64) - if err != nil { - return err - } - valSli = append(valSli, d) - } - structField.Set(reflect.ValueOf(valSli)) - case reflect.String: - valSli := make([]string, 0, len(filtered)) - for i := 0; i < len(filtered); i++ { - valSli = append(valSli, filtered[i]) - } - structField.Set(reflect.ValueOf(valSli)) - default: - sliceOf := structField.Type().Elem().Kind() - numElems := len(filtered) - slice := reflect.MakeSlice(structField.Type(), len(filtered), len(filtered)) - for i := 0; i < numElems; i++ { - if err := setWithProperType(sliceOf, filtered[i:], slice.Index(i), ""); err != nil { - return err - } - } - structField.Set(slice) - } - default: - return errors.New("Unknown type") - } - return nil -} - -func setIntField(val string, bitSize int, field reflect.Value) error { - if val == "" { - val = "0" - } - intVal, err := strconv.ParseInt(val, 10, bitSize) - if err == nil { - field.SetInt(intVal) - } - return errors.WithStack(err) -} - -func setUintField(val string, bitSize int, field reflect.Value) error { - if val == "" { - val = "0" - } - uintVal, err := strconv.ParseUint(val, 10, bitSize) - if err == nil { - field.SetUint(uintVal) - } - return errors.WithStack(err) -} - -func setBoolField(val string, field reflect.Value) error { - if val == "" { - val = "false" - } - boolVal, err := strconv.ParseBool(val) - if err == nil { - field.SetBool(boolVal) - } - return nil -} - -func setFloatField(val string, bitSize int, field reflect.Value) error { - if val == "" { - val = "0.0" - } - floatVal, err := strconv.ParseFloat(val, bitSize) - if err == nil { - field.SetFloat(floatVal) - } - return errors.WithStack(err) -} - -func setTimeField(val string, structField reflect.StructField, value reflect.Value) error { - timeFormat := structField.Tag.Get("time_format") - if timeFormat == "" { - return errors.New("Blank time format") - } - - if val == "" { - value.Set(reflect.ValueOf(time.Time{})) - return nil - } - - l := time.Local - if isUTC, _ := strconv.ParseBool(structField.Tag.Get("time_utc")); isUTC { - l = time.UTC - } - - if locTag := structField.Tag.Get("time_location"); locTag != "" { - loc, err := time.LoadLocation(locTag) - if err != nil { - return errors.WithStack(err) - } - l = loc - } - - t, err := time.ParseInLocation(timeFormat, val, l) - if err != nil { - return errors.WithStack(err) - } - - value.Set(reflect.ValueOf(t)) - return nil -} - -func filterEmpty(val []string) []string { - filtered := make([]string, 0, len(val)) - for _, v := range val { - if v != "" { - filtered = append(filtered, v) - } - } - return filtered -} diff --git a/pkg/net/http/blademaster/binding/json.go b/pkg/net/http/blademaster/binding/json.go deleted file mode 100644 index f01e479b3..000000000 --- a/pkg/net/http/blademaster/binding/json.go +++ /dev/null @@ -1,22 +0,0 @@ -package binding - -import ( - "encoding/json" - "net/http" - - "github.com/pkg/errors" -) - -type jsonBinding struct{} - -func (jsonBinding) Name() string { - return "json" -} - -func (jsonBinding) Bind(req *http.Request, obj interface{}) error { - decoder := json.NewDecoder(req.Body) - if err := decoder.Decode(obj); err != nil { - return errors.WithStack(err) - } - return validate(obj) -} diff --git a/pkg/net/http/blademaster/binding/query.go b/pkg/net/http/blademaster/binding/query.go deleted file mode 100644 index b169436eb..000000000 --- a/pkg/net/http/blademaster/binding/query.go +++ /dev/null @@ -1,19 +0,0 @@ -package binding - -import ( - "net/http" -) - -type queryBinding struct{} - -func (queryBinding) Name() string { - return "query" -} - -func (queryBinding) Bind(req *http.Request, obj interface{}) error { - values := req.URL.Query() - if err := mapForm(obj, values); err != nil { - return err - } - return validate(obj) -} diff --git a/pkg/net/http/blademaster/binding/tags.go b/pkg/net/http/blademaster/binding/tags.go deleted file mode 100644 index 535bd8624..000000000 --- a/pkg/net/http/blademaster/binding/tags.go +++ /dev/null @@ -1,44 +0,0 @@ -// Copyright 2011 The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - -package binding - -import ( - "strings" -) - -// tagOptions is the string following a comma in a struct field's "json" -// tag, or the empty string. It does not include the leading comma. -type tagOptions string - -// parseTag splits a struct field's json tag into its name and -// comma-separated options. -func parseTag(tag string) (string, tagOptions) { - if idx := strings.Index(tag, ","); idx != -1 { - return tag[:idx], tagOptions(tag[idx+1:]) - } - return tag, tagOptions("") -} - -// Contains reports whether a comma-separated list of options -// contains a particular substr flag. substr must be surrounded by a -// string boundary or commas. -func (o tagOptions) Contains(optionName string) bool { - if len(o) == 0 { - return false - } - s := string(o) - for s != "" { - var next string - i := strings.Index(s, ",") - if i >= 0 { - s, next = s[:i], s[i+1:] - } - if s == optionName { - return true - } - s = next - } - return false -} diff --git a/pkg/net/http/blademaster/binding/validate_test.go b/pkg/net/http/blademaster/binding/validate_test.go deleted file mode 100644 index ac1793713..000000000 --- a/pkg/net/http/blademaster/binding/validate_test.go +++ /dev/null @@ -1,209 +0,0 @@ -package binding - -import ( - "bytes" - "testing" - "time" - - "github.com/stretchr/testify/assert" -) - -type testInterface interface { - String() string -} - -type substructNoValidation struct { - IString string - IInt int -} - -type mapNoValidationSub map[string]substructNoValidation - -type structNoValidationValues struct { - substructNoValidation - - Boolean bool - - Uinteger uint - Integer int - Integer8 int8 - Integer16 int16 - Integer32 int32 - Integer64 int64 - Uinteger8 uint8 - Uinteger16 uint16 - Uinteger32 uint32 - Uinteger64 uint64 - - Float32 float32 - Float64 float64 - - String string - - Date time.Time - - Struct substructNoValidation - InlinedStruct struct { - String []string - Integer int - } - - IntSlice []int - IntPointerSlice []*int - StructPointerSlice []*substructNoValidation - StructSlice []substructNoValidation - InterfaceSlice []testInterface - - UniversalInterface interface{} - CustomInterface testInterface - - FloatMap map[string]float32 - StructMap mapNoValidationSub -} - -func createNoValidationValues() structNoValidationValues { - integer := 1 - s := structNoValidationValues{ - Boolean: true, - Uinteger: 1 << 29, - Integer: -10000, - Integer8: 120, - Integer16: -20000, - Integer32: 1 << 29, - Integer64: 1 << 61, - Uinteger8: 250, - Uinteger16: 50000, - Uinteger32: 1 << 31, - Uinteger64: 1 << 62, - Float32: 123.456, - Float64: 123.456789, - String: "text", - Date: time.Time{}, - CustomInterface: &bytes.Buffer{}, - Struct: substructNoValidation{}, - IntSlice: []int{-3, -2, 1, 0, 1, 2, 3}, - IntPointerSlice: []*int{&integer}, - StructSlice: []substructNoValidation{}, - UniversalInterface: 1.2, - FloatMap: map[string]float32{ - "foo": 1.23, - "bar": 232.323, - }, - StructMap: mapNoValidationSub{ - "foo": substructNoValidation{}, - "bar": substructNoValidation{}, - }, - // StructPointerSlice []noValidationSub - // InterfaceSlice []testInterface - } - s.InlinedStruct.Integer = 1000 - s.InlinedStruct.String = []string{"first", "second"} - s.IString = "substring" - s.IInt = 987654 - return s -} - -func TestValidateNoValidationValues(t *testing.T) { - origin := createNoValidationValues() - test := createNoValidationValues() - empty := structNoValidationValues{} - - assert.Nil(t, validate(test)) - assert.Nil(t, validate(&test)) - assert.Nil(t, validate(empty)) - assert.Nil(t, validate(&empty)) - - assert.Equal(t, origin, test) -} - -type structNoValidationPointer struct { - // substructNoValidation - - Boolean bool - - Uinteger *uint - Integer *int - Integer8 *int8 - Integer16 *int16 - Integer32 *int32 - Integer64 *int64 - Uinteger8 *uint8 - Uinteger16 *uint16 - Uinteger32 *uint32 - Uinteger64 *uint64 - - Float32 *float32 - Float64 *float64 - - String *string - - Date *time.Time - - Struct *substructNoValidation - - IntSlice *[]int - IntPointerSlice *[]*int - StructPointerSlice *[]*substructNoValidation - StructSlice *[]substructNoValidation - InterfaceSlice *[]testInterface - - FloatMap *map[string]float32 - StructMap *mapNoValidationSub -} - -func TestValidateNoValidationPointers(t *testing.T) { - //origin := createNoValidation_values() - //test := createNoValidation_values() - empty := structNoValidationPointer{} - - //assert.Nil(t, validate(test)) - //assert.Nil(t, validate(&test)) - assert.Nil(t, validate(empty)) - assert.Nil(t, validate(&empty)) - - //assert.Equal(t, origin, test) -} - -type Object map[string]interface{} - -func TestValidatePrimitives(t *testing.T) { - obj := Object{"foo": "bar", "bar": 1} - assert.NoError(t, validate(obj)) - assert.NoError(t, validate(&obj)) - assert.Equal(t, obj, Object{"foo": "bar", "bar": 1}) - - obj2 := []Object{{"foo": "bar", "bar": 1}, {"foo": "bar", "bar": 1}} - assert.NoError(t, validate(obj2)) - assert.NoError(t, validate(&obj2)) - - nu := 10 - assert.NoError(t, validate(nu)) - assert.NoError(t, validate(&nu)) - assert.Equal(t, nu, 10) - - str := "value" - assert.NoError(t, validate(str)) - assert.NoError(t, validate(&str)) - assert.Equal(t, str, "value") -} - -// structCustomValidation is a helper struct we use to check that -// custom validation can be registered on it. -// The `notone` binding directive is for custom validation and registered later. -// type structCustomValidation struct { -// Integer int `binding:"notone"` -// } - -// notOne is a custom validator meant to be used with `validator.v8` library. -// The method signature for `v9` is significantly different and this function -// would need to be changed for tests to pass after upgrade. -// See https://github.com/gin-gonic/gin/pull/1015. -// func notOne( -// v *validator.Validate, topStruct reflect.Value, currentStructOrField reflect.Value, -// field reflect.Value, fieldType reflect.Type, fieldKind reflect.Kind, param string, -// ) bool { -// if val, ok := field.Interface().(int); ok { -// return val != 1 -// } -// return false -// } diff --git a/pkg/net/http/blademaster/binding/xml.go b/pkg/net/http/blademaster/binding/xml.go deleted file mode 100644 index 99b303c2f..000000000 --- a/pkg/net/http/blademaster/binding/xml.go +++ /dev/null @@ -1,22 +0,0 @@ -package binding - -import ( - "encoding/xml" - "net/http" - - "github.com/pkg/errors" -) - -type xmlBinding struct{} - -func (xmlBinding) Name() string { - return "xml" -} - -func (xmlBinding) Bind(req *http.Request, obj interface{}) error { - decoder := xml.NewDecoder(req.Body) - if err := decoder.Decode(obj); err != nil { - return errors.WithStack(err) - } - return validate(obj) -} diff --git a/pkg/net/http/blademaster/client.go b/pkg/net/http/blademaster/client.go deleted file mode 100644 index 68d0ea6a1..000000000 --- a/pkg/net/http/blademaster/client.go +++ /dev/null @@ -1,365 +0,0 @@ -package blademaster - -import ( - "bytes" - "context" - "crypto/tls" - "encoding/json" - "fmt" - "io" - "net" - xhttp "net/http" - "net/url" - "os" - "runtime" - "strconv" - "strings" - "sync" - "time" - - "github.com/go-kratos/kratos/pkg/conf/env" - "github.com/go-kratos/kratos/pkg/net/metadata" - "github.com/go-kratos/kratos/pkg/net/netutil/breaker" - xtime "github.com/go-kratos/kratos/pkg/time" - - "github.com/gogo/protobuf/proto" - pkgerr "github.com/pkg/errors" -) - -const ( - _minRead = 16 * 1024 // 16kb -) - -var ( - _noKickUserAgent = "blademaster" -) - -func init() { - n, err := os.Hostname() - if err == nil { - _noKickUserAgent = _noKickUserAgent + runtime.Version() + " " + n - } -} - -// ClientConfig is http client conf. -type ClientConfig struct { - Dial xtime.Duration - Timeout xtime.Duration - KeepAlive xtime.Duration - Breaker *breaker.Config - URL map[string]*ClientConfig - Host map[string]*ClientConfig -} - -// Client is http client. -type Client struct { - conf *ClientConfig - client *xhttp.Client - dialer *net.Dialer - transport xhttp.RoundTripper - - urlConf map[string]*ClientConfig - hostConf map[string]*ClientConfig - mutex sync.RWMutex - breaker *breaker.Group -} - -// NewClient new a http client. -func NewClient(c *ClientConfig) *Client { - client := new(Client) - client.conf = c - client.dialer = &net.Dialer{ - Timeout: time.Duration(c.Dial), - KeepAlive: time.Duration(c.KeepAlive), - } - - originTransport := &xhttp.Transport{ - DialContext: client.dialer.DialContext, - TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, - } - - // wraps RoundTripper for tracer - client.transport = &TraceTransport{RoundTripper: originTransport} - client.client = &xhttp.Client{ - Transport: client.transport, - } - client.urlConf = make(map[string]*ClientConfig) - client.hostConf = make(map[string]*ClientConfig) - client.breaker = breaker.NewGroup(c.Breaker) - if c.Timeout <= 0 { - panic("must config http timeout!!!") - } - for uri, cfg := range c.URL { - client.urlConf[uri] = cfg - } - for host, cfg := range c.Host { - client.hostConf[host] = cfg - } - return client -} - -// SetTransport set client transport -func (client *Client) SetTransport(t xhttp.RoundTripper) { - client.transport = t - client.client.Transport = t -} - -// SetConfig set client config. -func (client *Client) SetConfig(c *ClientConfig) { - client.mutex.Lock() - if c.Timeout > 0 { - client.conf.Timeout = c.Timeout - } - if c.KeepAlive > 0 { - client.dialer.KeepAlive = time.Duration(c.KeepAlive) - client.conf.KeepAlive = c.KeepAlive - } - if c.Dial > 0 { - client.dialer.Timeout = time.Duration(c.Dial) - client.conf.Timeout = c.Dial - } - if c.Breaker != nil { - client.conf.Breaker = c.Breaker - client.breaker.Reload(c.Breaker) - } - for uri, cfg := range c.URL { - client.urlConf[uri] = cfg - } - for host, cfg := range c.Host { - client.hostConf[host] = cfg - } - client.mutex.Unlock() -} - -// NewRequest new http request with method, uri, ip, values and headers. -// TODO(zhoujiahui): param realIP should be removed later. -func (client *Client) NewRequest(method, uri, realIP string, params url.Values) (req *xhttp.Request, err error) { - if method == xhttp.MethodGet { - req, err = xhttp.NewRequest(xhttp.MethodGet, fmt.Sprintf("%s?%s", uri, params.Encode()), nil) - } else { - req, err = xhttp.NewRequest(xhttp.MethodPost, uri, strings.NewReader(params.Encode())) - } - if err != nil { - err = pkgerr.Wrapf(err, "method:%s,uri:%s", method, uri) - return - } - const ( - _contentType = "Content-Type" - _urlencoded = "application/x-www-form-urlencoded" - _userAgent = "User-Agent" - ) - if method == xhttp.MethodPost { - req.Header.Set(_contentType, _urlencoded) - } - if realIP != "" { - req.Header.Set(_httpHeaderRemoteIP, realIP) - } - req.Header.Set(_userAgent, _noKickUserAgent+" "+env.AppID) - return -} - -// Get issues a GET to the specified URL. -func (client *Client) Get(c context.Context, uri, ip string, params url.Values, res interface{}) (err error) { - req, err := client.NewRequest(xhttp.MethodGet, uri, ip, params) - if err != nil { - return - } - return client.Do(c, req, res) -} - -// Post issues a Post to the specified URL. -func (client *Client) Post(c context.Context, uri, ip string, params url.Values, res interface{}) (err error) { - req, err := client.NewRequest(xhttp.MethodPost, uri, ip, params) - if err != nil { - return - } - return client.Do(c, req, res) -} - -// RESTfulGet issues a RESTful GET to the specified URL. -func (client *Client) RESTfulGet(c context.Context, uri, ip string, params url.Values, res interface{}, v ...interface{}) (err error) { - req, err := client.NewRequest(xhttp.MethodGet, fmt.Sprintf(uri, v...), ip, params) - if err != nil { - return - } - return client.Do(c, req, res, uri) -} - -// RESTfulPost issues a RESTful Post to the specified URL. -func (client *Client) RESTfulPost(c context.Context, uri, ip string, params url.Values, res interface{}, v ...interface{}) (err error) { - req, err := client.NewRequest(xhttp.MethodPost, fmt.Sprintf(uri, v...), ip, params) - if err != nil { - return - } - return client.Do(c, req, res, uri) -} - -// Raw sends an HTTP request and returns bytes response -func (client *Client) Raw(c context.Context, req *xhttp.Request, v ...string) (bs []byte, err error) { - var ( - ok bool - code string - cancel func() - resp *xhttp.Response - config *ClientConfig - timeout time.Duration - uri = fmt.Sprintf("%s://%s%s", req.URL.Scheme, req.Host, req.URL.Path) - ) - // NOTE fix prom & config uri key. - if len(v) == 1 { - uri = v[0] - } - // breaker - brk := client.breaker.Get(uri) - if err = brk.Allow(); err != nil { - code = "breaker" - _metricClientReqCodeTotal.Inc(uri, req.Method, code) - return - } - defer client.onBreaker(brk, &err) - // stat - now := time.Now() - defer func() { - _metricClientReqDur.Observe(int64(time.Since(now)/time.Millisecond), uri, req.Method) - if code != "" { - _metricClientReqCodeTotal.Inc(uri, req.Method, code) - } - }() - // get config - // 1.url config 2.host config 3.default - client.mutex.RLock() - if config, ok = client.urlConf[uri]; !ok { - if config, ok = client.hostConf[req.Host]; !ok { - config = client.conf - } - } - client.mutex.RUnlock() - // timeout - deliver := true - timeout = time.Duration(config.Timeout) - if deadline, ok := c.Deadline(); ok { - if ctimeout := time.Until(deadline); ctimeout < timeout { - // deliver small timeout - timeout = ctimeout - deliver = false - } - } - if deliver { - c, cancel = context.WithTimeout(c, timeout) - defer cancel() - } - setTimeout(req, timeout) - req = req.WithContext(c) - setCaller(req) - metadata.Range(c, - func(key string, value interface{}) { - setMetadata(req, key, value) - }, - metadata.IsOutgoingKey) - if resp, err = client.client.Do(req); err != nil { - err = pkgerr.Wrapf(err, "host:%s, url:%s", req.URL.Host, realURL(req)) - code = "failed" - return - } - defer resp.Body.Close() - if resp.StatusCode >= xhttp.StatusBadRequest { - err = pkgerr.Errorf("incorrect http status:%d host:%s, url:%s", resp.StatusCode, req.URL.Host, realURL(req)) - code = strconv.Itoa(resp.StatusCode) - return - } - if bs, err = readAll(resp.Body, _minRead); err != nil { - err = pkgerr.Wrapf(err, "host:%s, url:%s", req.URL.Host, realURL(req)) - return - } - return -} - -// Do sends an HTTP request and returns an HTTP json response. -func (client *Client) Do(c context.Context, req *xhttp.Request, res interface{}, v ...string) (err error) { - var bs []byte - if bs, err = client.Raw(c, req, v...); err != nil { - return - } - if res != nil { - if err = json.Unmarshal(bs, res); err != nil { - err = pkgerr.Wrapf(err, "host:%s, url:%s", req.URL.Host, realURL(req)) - } - } - return -} - -// JSON sends an HTTP request and returns an HTTP json response. -func (client *Client) JSON(c context.Context, req *xhttp.Request, res interface{}, v ...string) (err error) { - var bs []byte - if bs, err = client.Raw(c, req, v...); err != nil { - return - } - if res != nil { - if err = json.Unmarshal(bs, res); err != nil { - err = pkgerr.Wrapf(err, "host:%s, url:%s", req.URL.Host, realURL(req)) - } - } - return -} - -// PB sends an HTTP request and returns an HTTP proto response. -func (client *Client) PB(c context.Context, req *xhttp.Request, res proto.Message, v ...string) (err error) { - var bs []byte - if bs, err = client.Raw(c, req, v...); err != nil { - return - } - if res != nil { - if err = proto.Unmarshal(bs, res); err != nil { - err = pkgerr.Wrapf(err, "host:%s, url:%s", req.URL.Host, realURL(req)) - } - } - return -} - -func (client *Client) onBreaker(breaker breaker.Breaker, err *error) { - if err != nil && *err != nil { - breaker.MarkFailed() - } else { - breaker.MarkSuccess() - } -} - -// realUrl return url with http://host/params. -func realURL(req *xhttp.Request) string { - if req.Method == xhttp.MethodGet { - return req.URL.String() - } else if req.Method == xhttp.MethodPost { - ru := req.URL.Path - if req.Body != nil { - rd, ok := req.Body.(io.Reader) - if ok { - buf := bytes.NewBuffer([]byte{}) - buf.ReadFrom(rd) - ru = ru + "?" + buf.String() - } - } - return ru - } - return req.URL.Path -} - -// readAll reads from r until an error or EOF and returns the data it read -// from the internal buffer allocated with a specified capacity. -func readAll(r io.Reader, capacity int64) (b []byte, err error) { - buf := bytes.NewBuffer(make([]byte, 0, capacity)) - // If the buffer overflows, we will get bytes.ErrTooLarge. - // Return that as an error. Any other panic remains. - defer func() { - e := recover() - if e == nil { - return - } - if panicErr, ok := e.(error); ok && panicErr == bytes.ErrTooLarge { - err = panicErr - } else { - panic(e) - } - }() - _, err = buf.ReadFrom(r) - return buf.Bytes(), err -} diff --git a/pkg/net/http/blademaster/context.go b/pkg/net/http/blademaster/context.go deleted file mode 100644 index fc6b544f9..000000000 --- a/pkg/net/http/blademaster/context.go +++ /dev/null @@ -1,408 +0,0 @@ -package blademaster - -import ( - "context" - "math" - "net/http" - "strconv" - "strings" - "sync" - "text/template" - - "github.com/go-kratos/kratos/pkg/net/metadata" - - "github.com/go-kratos/kratos/pkg/ecode" - "github.com/go-kratos/kratos/pkg/net/http/blademaster/binding" - "github.com/go-kratos/kratos/pkg/net/http/blademaster/render" - - "github.com/gogo/protobuf/proto" - "github.com/gogo/protobuf/types" - "github.com/pkg/errors" -) - -const ( - _abortIndex int8 = math.MaxInt8 / 2 -) - -var ( - _openParen = []byte("(") - _closeParen = []byte(")") -) - -// Context is the most important part. It allows us to pass variables between -// middleware, manage the flow, validate the JSON of a request and render a -// JSON response for example. -type Context struct { - context.Context - - Request *http.Request - Writer http.ResponseWriter - - // flow control - index int8 - handlers []HandlerFunc - - // Keys is a key/value pair exclusively for the context of each request. - Keys map[string]interface{} - // This mutex protect Keys map - keysMutex sync.RWMutex - - Error error - - method string - engine *Engine - - RoutePath string - - Params Params -} - -/************************************/ -/********** CONTEXT CREATION ********/ -/************************************/ -func (c *Context) reset() { - c.Context = nil - c.index = -1 - c.handlers = nil - c.Keys = nil - c.Error = nil - c.method = "" - c.RoutePath = "" - c.Params = c.Params[0:0] -} - -/************************************/ -/*********** FLOW CONTROL ***********/ -/************************************/ - -// Next should be used only inside middleware. -// It executes the pending handlers in the chain inside the calling handler. -// See example in godoc. -func (c *Context) Next() { - c.index++ - for c.index < int8(len(c.handlers)) { - c.handlers[c.index](c) - c.index++ - } -} - -// Abort prevents pending handlers from being called. Note that this will not stop the current handler. -// Let's say you have an authorization middleware that validates that the current request is authorized. -// If the authorization fails (ex: the password does not match), call Abort to ensure the remaining handlers -// for this request are not called. -func (c *Context) Abort() { - c.index = _abortIndex -} - -// AbortWithStatus calls `Abort()` and writes the headers with the specified status code. -// For example, a failed attempt to authenticate a request could use: context.AbortWithStatus(401). -func (c *Context) AbortWithStatus(code int) { - c.Status(code) - c.Abort() -} - -// IsAborted returns true if the current context was aborted. -func (c *Context) IsAborted() bool { - return c.index >= _abortIndex -} - -/************************************/ -/******** METADATA MANAGEMENT********/ -/************************************/ - -// Set is used to store a new key/value pair exclusively for this context. -// It also lazy initializes c.Keys if it was not used previously. -func (c *Context) Set(key string, value interface{}) { - c.keysMutex.Lock() - if c.Keys == nil { - c.Keys = make(map[string]interface{}) - } - c.Keys[key] = value - c.keysMutex.Unlock() -} - -// Get returns the value for the given key, ie: (value, true). -// If the value does not exists it returns (nil, false) -func (c *Context) Get(key string) (value interface{}, exists bool) { - c.keysMutex.RLock() - value, exists = c.Keys[key] - c.keysMutex.RUnlock() - return -} - -// GetString returns the value associated with the key as a string. -func (c *Context) GetString(key string) (s string) { - if val, ok := c.Get(key); ok && val != nil { - s, _ = val.(string) - } - return -} - -// GetBool returns the value associated with the key as a boolean. -func (c *Context) GetBool(key string) (b bool) { - if val, ok := c.Get(key); ok && val != nil { - b, _ = val.(bool) - } - return -} - -// GetInt returns the value associated with the key as an integer. -func (c *Context) GetInt(key string) (i int) { - if val, ok := c.Get(key); ok && val != nil { - i, _ = val.(int) - } - return -} - -// GetUint returns the value associated with the key as an unsigned integer. -func (c *Context) GetUint(key string) (ui uint) { - if val, ok := c.Get(key); ok && val != nil { - ui, _ = val.(uint) - } - return -} - -// GetInt64 returns the value associated with the key as an integer. -func (c *Context) GetInt64(key string) (i64 int64) { - if val, ok := c.Get(key); ok && val != nil { - i64, _ = val.(int64) - } - return -} - -// GetUint64 returns the value associated with the key as an unsigned integer. -func (c *Context) GetUint64(key string) (ui64 uint64) { - if val, ok := c.Get(key); ok && val != nil { - ui64, _ = val.(uint64) - } - return -} - -// GetFloat64 returns the value associated with the key as a float64. -func (c *Context) GetFloat64(key string) (f64 float64) { - if val, ok := c.Get(key); ok && val != nil { - f64, _ = val.(float64) - } - return -} - -/************************************/ -/******** RESPONSE RENDERING ********/ -/************************************/ - -// bodyAllowedForStatus is a copy of http.bodyAllowedForStatus non-exported function. -func bodyAllowedForStatus(status int) bool { - switch { - case status >= 100 && status <= 199: - return false - case status == 204: - return false - case status == 304: - return false - } - return true -} - -// Status sets the HTTP response code. -func (c *Context) Status(code int) { - c.Writer.WriteHeader(code) -} - -// Render http response with http code by a render instance. -func (c *Context) Render(code int, r render.Render) { - r.WriteContentType(c.Writer) - if code > 0 { - c.Status(code) - } - - if !bodyAllowedForStatus(code) { - return - } - - params := c.Request.Form - cb := template.JSEscapeString(params.Get("callback")) - jsonp := cb != "" - if jsonp { - c.Writer.Write([]byte(cb)) - c.Writer.Write(_openParen) - } - - if err := r.Render(c.Writer); err != nil { - c.Error = err - return - } - - if jsonp { - if _, err := c.Writer.Write(_closeParen); err != nil { - c.Error = errors.WithStack(err) - } - } -} - -// JSON serializes the given struct as JSON into the response body. -// It also sets the Content-Type as "application/json". -func (c *Context) JSON(data interface{}, err error) { - code := http.StatusOK - c.Error = err - bcode := ecode.Cause(err) - // TODO app allow 5xx? - /* - if bcode.Code() == -500 { - code = http.StatusServiceUnavailable - } - */ - writeStatusCode(c.Writer, bcode.Code()) - c.Render(code, render.JSON{ - Code: bcode.Code(), - Message: bcode.Message(), - Data: data, - }) -} - -// JSONMap serializes the given map as map JSON into the response body. -// It also sets the Content-Type as "application/json". -func (c *Context) JSONMap(data map[string]interface{}, err error) { - code := http.StatusOK - c.Error = err - bcode := ecode.Cause(err) - // TODO app allow 5xx? - /* - if bcode.Code() == -500 { - code = http.StatusServiceUnavailable - } - */ - writeStatusCode(c.Writer, bcode.Code()) - data["code"] = bcode.Code() - if _, ok := data["message"]; !ok { - data["message"] = bcode.Message() - } - c.Render(code, render.MapJSON(data)) -} - -// XML serializes the given struct as XML into the response body. -// It also sets the Content-Type as "application/xml". -func (c *Context) XML(data interface{}, err error) { - code := http.StatusOK - c.Error = err - bcode := ecode.Cause(err) - // TODO app allow 5xx? - /* - if bcode.Code() == -500 { - code = http.StatusServiceUnavailable - } - */ - writeStatusCode(c.Writer, bcode.Code()) - c.Render(code, render.XML{ - Code: bcode.Code(), - Message: bcode.Message(), - Data: data, - }) -} - -// Protobuf serializes the given struct as PB into the response body. -// It also sets the ContentType as "application/x-protobuf". -func (c *Context) Protobuf(data proto.Message, err error) { - var ( - bytes []byte - ) - - code := http.StatusOK - c.Error = err - bcode := ecode.Cause(err) - - any := new(types.Any) - if data != nil { - if bytes, err = proto.Marshal(data); err != nil { - c.Error = errors.WithStack(err) - return - } - any.TypeUrl = "type.googleapis.com/" + proto.MessageName(data) - any.Value = bytes - } - writeStatusCode(c.Writer, bcode.Code()) - c.Render(code, render.PB{ - Code: int64(bcode.Code()), - Message: bcode.Message(), - Data: any, - }) -} - -// Bytes writes some data into the body stream and updates the HTTP code. -func (c *Context) Bytes(code int, contentType string, data ...[]byte) { - c.Render(code, render.Data{ - ContentType: contentType, - Data: data, - }) -} - -// String writes the given string into the response body. -func (c *Context) String(code int, format string, values ...interface{}) { - c.Render(code, render.String{Format: format, Data: values}) -} - -// Redirect returns a HTTP redirect to the specific location. -func (c *Context) Redirect(code int, location string) { - c.Render(-1, render.Redirect{ - Code: code, - Location: location, - Request: c.Request, - }) -} - -// BindWith bind req arg with parser. -func (c *Context) BindWith(obj interface{}, b binding.Binding) error { - return c.mustBindWith(obj, b) -} - -// Bind checks the Content-Type to select a binding engine automatically, -// Depending the "Content-Type" header different bindings are used: -// "application/json" --> JSON binding -// "application/xml" --> XML binding -// otherwise --> returns an error. -// It parses the request's body as JSON if Content-Type == "application/json" using JSON or XML as a JSON input. -// It decodes the json payload into the struct specified as a pointer. -// It writes a 400 error and sets Content-Type header "text/plain" in the response if input is not valid. -func (c *Context) Bind(obj interface{}) error { - b := binding.Default(c.Request.Method, c.Request.Header.Get("Content-Type")) - return c.mustBindWith(obj, b) -} - -// mustBindWith binds the passed struct pointer using the specified binding engine. -// It will abort the request with HTTP 400 if any error ocurrs. -// See the binding package. -func (c *Context) mustBindWith(obj interface{}, b binding.Binding) (err error) { - if err = b.Bind(c.Request, obj); err != nil { - c.Error = ecode.RequestErr - c.Render(http.StatusOK, render.JSON{ - Code: ecode.RequestErr.Code(), - Message: err.Error(), - Data: nil, - }) - c.Abort() - } - return -} - -func writeStatusCode(w http.ResponseWriter, ecode int) { - header := w.Header() - header.Set("kratos-status-code", strconv.FormatInt(int64(ecode), 10)) -} - -// RemoteIP implements a best effort algorithm to return the real client IP, it parses -// X-Real-IP and X-Forwarded-For in order to work properly with reverse-proxies such us: nginx or haproxy. -// Use X-Forwarded-For before X-Real-Ip as nginx uses X-Real-Ip with the proxy's IP. -// Notice: metadata.RemoteIP take precedence over X-Forwarded-For and X-Real-Ip -func (c *Context) RemoteIP() (remoteIP string) { - remoteIP = metadata.String(c, metadata.RemoteIP) - if remoteIP != "" { - return - } - - remoteIP = c.Request.Header.Get("X-Forwarded-For") - remoteIP = strings.TrimSpace(strings.Split(remoteIP, ",")[0]) - if remoteIP == "" { - remoteIP = strings.TrimSpace(c.Request.Header.Get("X-Real-Ip")) - } - - return -} diff --git a/pkg/net/http/blademaster/cors.go b/pkg/net/http/blademaster/cors.go deleted file mode 100644 index 8192e0871..000000000 --- a/pkg/net/http/blademaster/cors.go +++ /dev/null @@ -1,249 +0,0 @@ -package blademaster - -import ( - "net/http" - "strconv" - "strings" - "time" - - "github.com/go-kratos/kratos/pkg/log" - - "github.com/pkg/errors" -) - -// CORSConfig represents all available options for the middleware. -type CORSConfig struct { - AllowAllOrigins bool - - // AllowedOrigins is a list of origins a cross-domain request can be executed from. - // If the special "*" value is present in the list, all origins will be allowed. - // Default value is [] - AllowOrigins []string - - // AllowOriginFunc is a custom function to validate the origin. It take the origin - // as argument and returns true if allowed or false otherwise. If this option is - // set, the content of AllowedOrigins is ignored. - AllowOriginFunc func(origin string) bool - - // AllowedMethods is a list of methods the client is allowed to use with - // cross-domain requests. Default value is simple methods (GET and POST) - AllowMethods []string - - // AllowedHeaders is list of non simple headers the client is allowed to use with - // cross-domain requests. - AllowHeaders []string - - // AllowCredentials indicates whether the request can include user credentials like - // cookies, HTTP authentication or client side SSL certificates. - AllowCredentials bool - - // ExposedHeaders indicates which headers are safe to expose to the API of a CORS - // API specification - ExposeHeaders []string - - // MaxAge indicates how long (in seconds) the results of a preflight request - // can be cached - MaxAge time.Duration -} - -type cors struct { - allowAllOrigins bool - allowCredentials bool - allowOriginFunc func(string) bool - allowOrigins []string - normalHeaders http.Header - preflightHeaders http.Header -} - -type converter func(string) string - -// Validate is check configuration of user defined. -func (c *CORSConfig) Validate() error { - if c.AllowAllOrigins && (c.AllowOriginFunc != nil || len(c.AllowOrigins) > 0) { - return errors.New("conflict settings: all origins are allowed. AllowOriginFunc or AllowedOrigins is not needed") - } - if !c.AllowAllOrigins && c.AllowOriginFunc == nil && len(c.AllowOrigins) == 0 { - return errors.New("conflict settings: all origins disabled") - } - for _, origin := range c.AllowOrigins { - if origin != "*" && !strings.HasPrefix(origin, "http://") && !strings.HasPrefix(origin, "https://") { - return errors.New("bad origin: origins must either be '*' or include http:// or https://") - } - } - return nil -} - -// CORS returns the location middleware with default configuration. -func CORS(allowOriginHosts []string) HandlerFunc { - config := &CORSConfig{ - AllowMethods: []string{"GET", "POST"}, - AllowHeaders: []string{"Origin", "Content-Length", "Content-Type"}, - AllowCredentials: true, - MaxAge: time.Duration(0), - AllowOriginFunc: func(origin string) bool { - for _, host := range allowOriginHosts { - if strings.HasSuffix(strings.ToLower(origin), host) { - return true - } - } - return false - }, - } - return newCORS(config) -} - -// newCORS returns the location middleware with user-defined custom configuration. -func newCORS(config *CORSConfig) HandlerFunc { - if err := config.Validate(); err != nil { - panic(err.Error()) - } - cors := &cors{ - allowOriginFunc: config.AllowOriginFunc, - allowAllOrigins: config.AllowAllOrigins, - allowCredentials: config.AllowCredentials, - allowOrigins: normalize(config.AllowOrigins), - normalHeaders: generateNormalHeaders(config), - preflightHeaders: generatePreflightHeaders(config), - } - - return func(c *Context) { - cors.applyCORS(c) - } -} - -func (cors *cors) applyCORS(c *Context) { - origin := c.Request.Header.Get("Origin") - if len(origin) == 0 { - // request is not a CORS request - return - } - if !cors.validateOrigin(origin) { - log.V(5).Info("The request's Origin header `%s` does not match any of allowed origins.", origin) - c.AbortWithStatus(http.StatusForbidden) - return - } - - if c.Request.Method == "OPTIONS" { - cors.handlePreflight(c) - defer c.AbortWithStatus(200) - } else { - cors.handleNormal(c) - } - - if !cors.allowAllOrigins { - header := c.Writer.Header() - header.Set("Access-Control-Allow-Origin", origin) - } -} - -func (cors *cors) validateOrigin(origin string) bool { - if cors.allowAllOrigins { - return true - } - for _, value := range cors.allowOrigins { - if value == origin { - return true - } - } - if cors.allowOriginFunc != nil { - return cors.allowOriginFunc(origin) - } - return false -} - -func (cors *cors) handlePreflight(c *Context) { - header := c.Writer.Header() - for key, value := range cors.preflightHeaders { - header[key] = value - } -} - -func (cors *cors) handleNormal(c *Context) { - header := c.Writer.Header() - for key, value := range cors.normalHeaders { - header[key] = value - } -} - -func generateNormalHeaders(c *CORSConfig) http.Header { - headers := make(http.Header) - if c.AllowCredentials { - headers.Set("Access-Control-Allow-Credentials", "true") - } - - // backport support for early browsers - if len(c.AllowMethods) > 0 { - allowMethods := convert(normalize(c.AllowMethods), strings.ToUpper) - value := strings.Join(allowMethods, ",") - headers.Set("Access-Control-Allow-Methods", value) - } - - if len(c.ExposeHeaders) > 0 { - exposeHeaders := convert(normalize(c.ExposeHeaders), http.CanonicalHeaderKey) - headers.Set("Access-Control-Expose-Headers", strings.Join(exposeHeaders, ",")) - } - if c.AllowAllOrigins { - headers.Set("Access-Control-Allow-Origin", "*") - } else { - headers.Set("Vary", "Origin") - } - return headers -} - -func generatePreflightHeaders(c *CORSConfig) http.Header { - headers := make(http.Header) - if c.AllowCredentials { - headers.Set("Access-Control-Allow-Credentials", "true") - } - if len(c.AllowMethods) > 0 { - allowMethods := convert(normalize(c.AllowMethods), strings.ToUpper) - value := strings.Join(allowMethods, ",") - headers.Set("Access-Control-Allow-Methods", value) - } - if len(c.AllowHeaders) > 0 { - allowHeaders := convert(normalize(c.AllowHeaders), http.CanonicalHeaderKey) - value := strings.Join(allowHeaders, ",") - headers.Set("Access-Control-Allow-Headers", value) - } - if c.MaxAge > time.Duration(0) { - value := strconv.FormatInt(int64(c.MaxAge/time.Second), 10) - headers.Set("Access-Control-Max-Age", value) - } - if c.AllowAllOrigins { - headers.Set("Access-Control-Allow-Origin", "*") - } else { - // Always set Vary headers - // see https://github.com/rs/cors/issues/10, - // https://github.com/rs/cors/commit/dbdca4d95feaa7511a46e6f1efb3b3aa505bc43f#commitcomment-12352001 - - headers.Add("Vary", "Origin") - headers.Add("Vary", "Access-Control-Request-Method") - headers.Add("Vary", "Access-Control-Request-Headers") - } - return headers -} - -func normalize(values []string) []string { - if values == nil { - return nil - } - distinctMap := make(map[string]bool, len(values)) - normalized := make([]string, 0, len(values)) - for _, value := range values { - value = strings.TrimSpace(value) - value = strings.ToLower(value) - if _, seen := distinctMap[value]; !seen { - normalized = append(normalized, value) - distinctMap[value] = true - } - } - return normalized -} - -func convert(s []string, c converter) []string { - var out []string - for _, i := range s { - out = append(out, c(i)) - } - return out -} diff --git a/pkg/net/http/blademaster/criticality.go b/pkg/net/http/blademaster/criticality.go deleted file mode 100644 index 0705ff0e7..000000000 --- a/pkg/net/http/blademaster/criticality.go +++ /dev/null @@ -1,21 +0,0 @@ -package blademaster - -import ( - criticalityPkg "github.com/go-kratos/kratos/pkg/net/criticality" - "github.com/go-kratos/kratos/pkg/net/metadata" - - "github.com/pkg/errors" -) - -// Criticality is -func Criticality(pathCriticality criticalityPkg.Criticality) HandlerFunc { - if !criticalityPkg.Exist(pathCriticality) { - panic(errors.Errorf("This criticality is not exist: %s", pathCriticality)) - } - return func(ctx *Context) { - md, ok := metadata.FromContext(ctx) - if ok { - md[metadata.Criticality] = string(pathCriticality) - } - } -} diff --git a/pkg/net/http/blademaster/csrf.go b/pkg/net/http/blademaster/csrf.go deleted file mode 100644 index 25482f5d7..000000000 --- a/pkg/net/http/blademaster/csrf.go +++ /dev/null @@ -1,64 +0,0 @@ -package blademaster - -import ( - "net/url" - "regexp" - "strings" - - "github.com/go-kratos/kratos/pkg/log" -) - -func matchHostSuffix(suffix string) func(*url.URL) bool { - return func(uri *url.URL) bool { - return strings.HasSuffix(strings.ToLower(uri.Host), suffix) - } -} - -func matchPattern(pattern *regexp.Regexp) func(*url.URL) bool { - return func(uri *url.URL) bool { - return pattern.MatchString(strings.ToLower(uri.String())) - } -} - -// CSRF returns the csrf middleware to prevent invalid cross site request. -// Only referer is checked currently. -func CSRF(allowHosts []string, allowPattern []string) HandlerFunc { - validations := []func(*url.URL) bool{} - - addHostSuffix := func(suffix string) { - validations = append(validations, matchHostSuffix(suffix)) - } - addPattern := func(pattern string) { - validations = append(validations, matchPattern(regexp.MustCompile(pattern))) - } - - for _, r := range allowHosts { - addHostSuffix(r) - } - for _, p := range allowPattern { - addPattern(p) - } - - return func(c *Context) { - referer := c.Request.Header.Get("Referer") - if referer == "" { - log.V(5).Info("The request's Referer or Origin header is empty.") - c.AbortWithStatus(403) - return - } - illegal := true - if uri, err := url.Parse(referer); err == nil && uri.Host != "" { - for _, validate := range validations { - if validate(uri) { - illegal = false - break - } - } - } - if illegal { - log.V(5).Info("The request's Referer header `%s` does not match any of allowed referers.", referer) - c.AbortWithStatus(403) - return - } - } -} diff --git a/pkg/net/http/blademaster/logger.go b/pkg/net/http/blademaster/logger.go deleted file mode 100644 index 7a0700826..000000000 --- a/pkg/net/http/blademaster/logger.go +++ /dev/null @@ -1,70 +0,0 @@ -package blademaster - -import ( - "fmt" - "strconv" - "time" - - "github.com/go-kratos/kratos/pkg/ecode" - "github.com/go-kratos/kratos/pkg/log" - "github.com/go-kratos/kratos/pkg/net/metadata" -) - -// Logger is logger middleware -func Logger() HandlerFunc { - const noUser = "no_user" - return func(c *Context) { - now := time.Now() - req := c.Request - path := req.URL.Path - params := req.Form - var quota float64 - if deadline, ok := c.Context.Deadline(); ok { - quota = time.Until(deadline).Seconds() - } - - c.Next() - - err := c.Error - cerr := ecode.Cause(err) - dt := time.Since(now) - caller := metadata.String(c, metadata.Caller) - if caller == "" { - caller = noUser - } - - if len(c.RoutePath) > 0 { - _metricServerReqCodeTotal.Inc(c.RoutePath[1:], caller, req.Method, strconv.FormatInt(int64(cerr.Code()), 10)) - _metricServerReqDur.Observe(int64(dt/time.Millisecond), c.RoutePath[1:], caller, req.Method) - } - - lf := log.Infov - errmsg := "" - isSlow := dt >= (time.Millisecond * 500) - if err != nil { - errmsg = err.Error() - lf = log.Errorv - if cerr.Code() > 0 { - lf = log.Warnv - } - } else { - if isSlow { - lf = log.Warnv - } - } - lf(c, - log.KVString("method", req.Method), - log.KVString("ip", c.RemoteIP()), - log.KVString("user", caller), - log.KVString("path", path), - log.KVString("params", params.Encode()), - log.KVInt("ret", cerr.Code()), - log.KVString("msg", cerr.Message()), - log.KVString("stack", fmt.Sprintf("%+v", err)), - log.KVString("err", errmsg), - log.KVFloat64("timeout_quota", quota), - log.KVFloat64("ts", dt.Seconds()), - log.KVString("source", "http-access-log"), - ) - } -} diff --git a/pkg/net/http/blademaster/metadata.go b/pkg/net/http/blademaster/metadata.go deleted file mode 100644 index 5befddd5f..000000000 --- a/pkg/net/http/blademaster/metadata.go +++ /dev/null @@ -1,123 +0,0 @@ -package blademaster - -import ( - "fmt" - "net" - "net/http" - "strconv" - "strings" - "time" - - "github.com/go-kratos/kratos/pkg/conf/env" - "github.com/go-kratos/kratos/pkg/log" - "github.com/go-kratos/kratos/pkg/net/criticality" - "github.com/go-kratos/kratos/pkg/net/metadata" - - "github.com/pkg/errors" -) - -const ( - // http head - _httpHeaderUser = "x1-bmspy-user" - _httpHeaderTimeout = "x1-bmspy-timeout" - _httpHeaderRemoteIP = "x-backend-bm-real-ip" - _httpHeaderRemoteIPPort = "x-backend-bm-real-ipport" -) - -const ( - _httpHeaderMetadata = "x-bm-metadata-" -) - -var _parser = map[string]func(string) interface{}{ - "mirror": func(mirrorStr string) interface{} { - if mirrorStr == "" { - return false - } - val, err := strconv.ParseBool(mirrorStr) - if err != nil { - log.Warn("blademaster: failed to parse mirror: %+v", errors.Wrap(err, mirrorStr)) - return false - } - if !val { - log.Warn("blademaster: request mirrorStr value :%s is false", mirrorStr) - } - return val - }, - "criticality": func(in string) interface{} { - if crtl := criticality.Criticality(in); crtl != criticality.EmptyCriticality { - return string(crtl) - } - return string(criticality.Critical) - }, -} - -func parseMetadataTo(req *http.Request, to metadata.MD) { - for rawKey := range req.Header { - key := strings.ReplaceAll(strings.TrimPrefix(strings.ToLower(rawKey), _httpHeaderMetadata), "-", "_") - rawValue := req.Header.Get(rawKey) - var value interface{} = rawValue - parser, ok := _parser[key] - if ok { - value = parser(rawValue) - } - to[key] = value - } - return -} - -func setMetadata(req *http.Request, key string, value interface{}) { - strV, ok := value.(string) - if !ok { - return - } - header := fmt.Sprintf("%s%s", _httpHeaderMetadata, strings.ReplaceAll(key, "_", "-")) - req.Header.Set(header, strV) -} - -// setCaller set caller into http request. -func setCaller(req *http.Request) { - req.Header.Set(_httpHeaderUser, env.AppID) -} - -// setTimeout set timeout into http request. -func setTimeout(req *http.Request, timeout time.Duration) { - td := int64(timeout / time.Millisecond) - req.Header.Set(_httpHeaderTimeout, strconv.FormatInt(td, 10)) -} - -// timeout get timeout from http request. -func timeout(req *http.Request) time.Duration { - to := req.Header.Get(_httpHeaderTimeout) - timeout, err := strconv.ParseInt(to, 10, 64) - if err == nil && timeout > 20 { - timeout -= 20 // reduce 20ms every time. - } - return time.Duration(timeout) * time.Millisecond -} - -// remoteIP implements a best effort algorithm to return the real client IP, it parses -// x-backend-bm-real-ip or X-Real-IP or X-Forwarded-For in order to work properly with reverse-proxies such us: nginx or haproxy. -// Use X-Forwarded-For before X-Real-Ip as nginx uses X-Real-Ip with the proxy's IP. -func remoteIP(req *http.Request) (remote string) { - if remote = req.Header.Get(_httpHeaderRemoteIP); remote != "" && remote != "null" { - return - } - var xff = req.Header.Get("X-Forwarded-For") - if idx := strings.IndexByte(xff, ','); idx > -1 { - if remote = strings.TrimSpace(xff[:idx]); remote != "" { - return - } - } - if remote = req.Header.Get("X-Real-IP"); remote != "" { - return - } - remote, _, _ = net.SplitHostPort(req.RemoteAddr) - return -} - -func remotePort(req *http.Request) (port string) { - if port = req.Header.Get(_httpHeaderRemoteIPPort); port != "" && port != "null" { - return - } - return -} diff --git a/pkg/net/http/blademaster/metrics.go b/pkg/net/http/blademaster/metrics.go deleted file mode 100644 index 87ee59167..000000000 --- a/pkg/net/http/blademaster/metrics.go +++ /dev/null @@ -1,48 +0,0 @@ -package blademaster - -import "github.com/go-kratos/kratos/pkg/stat/metric" - -const ( - clientNamespace = "http_client" - serverNamespace = "http_server" -) - -var ( - _metricServerReqDur = metric.NewHistogramVec(&metric.HistogramVecOpts{ - Namespace: serverNamespace, - Subsystem: "requests", - Name: "duration_ms", - Help: "http server requests duration(ms).", - Labels: []string{"path", "caller", "method"}, - Buckets: []float64{5, 10, 25, 50, 100, 250, 500, 1000}, - }) - _metricServerReqCodeTotal = metric.NewCounterVec(&metric.CounterVecOpts{ - Namespace: serverNamespace, - Subsystem: "requests", - Name: "code_total", - Help: "http server requests error count.", - Labels: []string{"path", "caller", "method", "code"}, - }) - _metricServerBBR = metric.NewCounterVec(&metric.CounterVecOpts{ - Namespace: serverNamespace, - Subsystem: "", - Name: "bbr_total", - Help: "http server bbr total.", - Labels: []string{"url", "method"}, - }) - _metricClientReqDur = metric.NewHistogramVec(&metric.HistogramVecOpts{ - Namespace: clientNamespace, - Subsystem: "requests", - Name: "duration_ms", - Help: "http client requests duration(ms).", - Labels: []string{"path", "method"}, - Buckets: []float64{5, 10, 25, 50, 100, 250, 500, 1000}, - }) - _metricClientReqCodeTotal = metric.NewCounterVec(&metric.CounterVecOpts{ - Namespace: clientNamespace, - Subsystem: "requests", - Name: "code_total", - Help: "http client requests code count.", - Labels: []string{"path", "method", "code"}, - }) -) diff --git a/pkg/net/http/blademaster/perf.go b/pkg/net/http/blademaster/perf.go deleted file mode 100644 index a15c5aac0..000000000 --- a/pkg/net/http/blademaster/perf.go +++ /dev/null @@ -1,63 +0,0 @@ -package blademaster - -import ( - "flag" - "net/http" - "net/http/pprof" - "os" - "sync" - - "github.com/go-kratos/kratos/pkg/conf/dsn" - - "github.com/pkg/errors" -) - -var ( - _perfOnce sync.Once - _perfDSN string -) - -func init() { - v := os.Getenv("HTTP_PERF") - flag.StringVar(&_perfDSN, "http.perf", v, "listen http perf dsn, or use HTTP_PERF env variable.") -} - -func startPerf(engine *Engine) { - _perfOnce.Do(func() { - if os.Getenv("HTTP_PERF") == "" { - prefixRouter := engine.Group("/debug/pprof") - { - prefixRouter.GET("/", pprofHandler(pprof.Index)) - prefixRouter.GET("/cmdline", pprofHandler(pprof.Cmdline)) - prefixRouter.GET("/profile", pprofHandler(pprof.Profile)) - prefixRouter.POST("/symbol", pprofHandler(pprof.Symbol)) - prefixRouter.GET("/symbol", pprofHandler(pprof.Symbol)) - prefixRouter.GET("/trace", pprofHandler(pprof.Trace)) - prefixRouter.GET("/allocs", pprofHandler(pprof.Handler("allocs").ServeHTTP)) - prefixRouter.GET("/block", pprofHandler(pprof.Handler("block").ServeHTTP)) - prefixRouter.GET("/goroutine", pprofHandler(pprof.Handler("goroutine").ServeHTTP)) - prefixRouter.GET("/heap", pprofHandler(pprof.Handler("heap").ServeHTTP)) - prefixRouter.GET("/mutex", pprofHandler(pprof.Handler("mutex").ServeHTTP)) - prefixRouter.GET("/threadcreate", pprofHandler(pprof.Handler("threadcreate").ServeHTTP)) - } - return - } - - go func() { - d, err := dsn.Parse(_perfDSN) - if err != nil { - panic(errors.Errorf("blademaster: http perf dsn must be tcp://$host:port, %s:error(%v)", _perfDSN, err)) - } - if err := http.ListenAndServe(d.Host, nil); err != nil { - panic(errors.Errorf("blademaster: listen %s: error(%v)", d.Host, err)) - } - }() - }) -} - -func pprofHandler(h http.HandlerFunc) HandlerFunc { - handler := http.HandlerFunc(h) - return func(c *Context) { - handler.ServeHTTP(c.Writer, c.Request) - } -} diff --git a/pkg/net/http/blademaster/prometheus.go b/pkg/net/http/blademaster/prometheus.go deleted file mode 100644 index 68af2ee58..000000000 --- a/pkg/net/http/blademaster/prometheus.go +++ /dev/null @@ -1,12 +0,0 @@ -package blademaster - -import ( - "github.com/prometheus/client_golang/prometheus/promhttp" -) - -func monitor() HandlerFunc { - return func(c *Context) { - h := promhttp.Handler() - h.ServeHTTP(c.Writer, c.Request) - } -} diff --git a/pkg/net/http/blademaster/ratelimit.go b/pkg/net/http/blademaster/ratelimit.go deleted file mode 100644 index e2347a46d..000000000 --- a/pkg/net/http/blademaster/ratelimit.go +++ /dev/null @@ -1,53 +0,0 @@ -package blademaster - -import ( - "fmt" - "sync/atomic" - "time" - - "github.com/go-kratos/kratos/pkg/log" - limit "github.com/go-kratos/kratos/pkg/ratelimit" - "github.com/go-kratos/kratos/pkg/ratelimit/bbr" -) - -// RateLimiter bbr middleware. -type RateLimiter struct { - group *bbr.Group - logTime int64 -} - -// NewRateLimiter return a ratelimit middleware. -func NewRateLimiter(conf *bbr.Config) (s *RateLimiter) { - return &RateLimiter{ - group: bbr.NewGroup(conf), - logTime: time.Now().UnixNano(), - } -} - -func (b *RateLimiter) printStats(routePath string, limiter limit.Limiter) { - now := time.Now().UnixNano() - if now-atomic.LoadInt64(&b.logTime) > int64(time.Second*3) { - atomic.StoreInt64(&b.logTime, now) - log.Info("http.bbr path:%s stat:%+v", routePath, limiter.(*bbr.BBR).Stat()) - } -} - -// Limit return a bm handler func. -func (b *RateLimiter) Limit() HandlerFunc { - return func(c *Context) { - uri := fmt.Sprintf("%s://%s%s", c.Request.URL.Scheme, c.Request.Host, c.Request.URL.Path) - limiter := b.group.Get(uri) - done, err := limiter.Allow(c) - if err != nil { - _metricServerBBR.Inc(uri, c.Request.Method) - c.JSON(nil, err) - c.Abort() - return - } - defer func() { - done(limit.DoneInfo{Op: limit.Success}) - b.printStats(uri, limiter) - }() - c.Next() - } -} diff --git a/pkg/net/http/blademaster/recovery.go b/pkg/net/http/blademaster/recovery.go deleted file mode 100644 index 239d496bb..000000000 --- a/pkg/net/http/blademaster/recovery.go +++ /dev/null @@ -1,32 +0,0 @@ -package blademaster - -import ( - "fmt" - "net/http/httputil" - "os" - "runtime" - - "github.com/go-kratos/kratos/pkg/log" -) - -// Recovery returns a middleware that recovers from any panics and writes a 500 if there was one. -func Recovery() HandlerFunc { - return func(c *Context) { - defer func() { - var rawReq []byte - if err := recover(); err != nil { - const size = 64 << 10 - buf := make([]byte, size) - buf = buf[:runtime.Stack(buf, false)] - if c.Request != nil { - rawReq, _ = httputil.DumpRequest(c.Request, false) - } - pl := fmt.Sprintf("http call panic: %s\n%v\n%s\n", string(rawReq), err, buf) - fmt.Fprintf(os.Stderr, pl) - log.Error(pl) - c.AbortWithStatus(500) - } - }() - c.Next() - } -} diff --git a/pkg/net/http/blademaster/render/data.go b/pkg/net/http/blademaster/render/data.go deleted file mode 100644 index d602350b0..000000000 --- a/pkg/net/http/blademaster/render/data.go +++ /dev/null @@ -1,30 +0,0 @@ -package render - -import ( - "net/http" - - "github.com/pkg/errors" -) - -// Data common bytes struct. -type Data struct { - ContentType string - Data [][]byte -} - -// Render (Data) writes data with custom ContentType. -func (r Data) Render(w http.ResponseWriter) (err error) { - r.WriteContentType(w) - for _, d := range r.Data { - if _, err = w.Write(d); err != nil { - err = errors.WithStack(err) - return - } - } - return -} - -// WriteContentType writes data with custom ContentType. -func (r Data) WriteContentType(w http.ResponseWriter) { - writeContentType(w, []string{r.ContentType}) -} diff --git a/pkg/net/http/blademaster/render/json.go b/pkg/net/http/blademaster/render/json.go deleted file mode 100644 index 5a5f23bff..000000000 --- a/pkg/net/http/blademaster/render/json.go +++ /dev/null @@ -1,58 +0,0 @@ -package render - -import ( - "encoding/json" - "net/http" - - "github.com/pkg/errors" -) - -var jsonContentType = []string{"application/json; charset=utf-8"} - -// JSON common json struct. -type JSON struct { - Code int `json:"code"` - Message string `json:"message"` - TTL int `json:"ttl"` - Data interface{} `json:"data,omitempty"` -} - -func writeJSON(w http.ResponseWriter, obj interface{}) (err error) { - var jsonBytes []byte - writeContentType(w, jsonContentType) - if jsonBytes, err = json.Marshal(obj); err != nil { - err = errors.WithStack(err) - return - } - if _, err = w.Write(jsonBytes); err != nil { - err = errors.WithStack(err) - } - return -} - -// Render (JSON) writes data with json ContentType. -func (r JSON) Render(w http.ResponseWriter) error { - // FIXME(zhoujiahui): the TTL field will be configurable in the future - if r.TTL <= 0 { - r.TTL = 1 - } - return writeJSON(w, r) -} - -// WriteContentType write json ContentType. -func (r JSON) WriteContentType(w http.ResponseWriter) { - writeContentType(w, jsonContentType) -} - -// MapJSON common map json struct. -type MapJSON map[string]interface{} - -// Render (MapJSON) writes data with json ContentType. -func (m MapJSON) Render(w http.ResponseWriter) error { - return writeJSON(w, m) -} - -// WriteContentType write json ContentType. -func (m MapJSON) WriteContentType(w http.ResponseWriter) { - writeContentType(w, jsonContentType) -} diff --git a/pkg/net/http/blademaster/render/protobuf.go b/pkg/net/http/blademaster/render/protobuf.go deleted file mode 100644 index 4664f2b5f..000000000 --- a/pkg/net/http/blademaster/render/protobuf.go +++ /dev/null @@ -1,38 +0,0 @@ -package render - -import ( - "net/http" - - "github.com/gogo/protobuf/proto" - "github.com/pkg/errors" -) - -var pbContentType = []string{"application/x-protobuf"} - -// Render (PB) writes data with protobuf ContentType. -func (r PB) Render(w http.ResponseWriter) error { - if r.TTL <= 0 { - r.TTL = 1 - } - return writePB(w, r) -} - -// WriteContentType write protobuf ContentType. -func (r PB) WriteContentType(w http.ResponseWriter) { - writeContentType(w, pbContentType) -} - -func writePB(w http.ResponseWriter, obj PB) (err error) { - var pbBytes []byte - writeContentType(w, pbContentType) - - if pbBytes, err = proto.Marshal(&obj); err != nil { - err = errors.WithStack(err) - return - } - - if _, err = w.Write(pbBytes); err != nil { - err = errors.WithStack(err) - } - return -} diff --git a/pkg/net/http/blademaster/render/redirect.go b/pkg/net/http/blademaster/render/redirect.go deleted file mode 100644 index 73e516d65..000000000 --- a/pkg/net/http/blademaster/render/redirect.go +++ /dev/null @@ -1,26 +0,0 @@ -package render - -import ( - "net/http" - - "github.com/pkg/errors" -) - -// Redirect render for redirect to specified location. -type Redirect struct { - Code int - Request *http.Request - Location string -} - -// Render (Redirect) redirect to specified location. -func (r Redirect) Render(w http.ResponseWriter) error { - if (r.Code < 300 || r.Code > 308) && r.Code != 201 { - return errors.Errorf("Cannot redirect with status code %d", r.Code) - } - http.Redirect(w, r.Request, r.Location, r.Code) - return nil -} - -// WriteContentType noneContentType. -func (r Redirect) WriteContentType(http.ResponseWriter) {} diff --git a/pkg/net/http/blademaster/render/render.go b/pkg/net/http/blademaster/render/render.go deleted file mode 100644 index 1cb40d409..000000000 --- a/pkg/net/http/blademaster/render/render.go +++ /dev/null @@ -1,30 +0,0 @@ -package render - -import ( - "net/http" -) - -// Render http response render. -type Render interface { - // Render render it to http response writer. - Render(http.ResponseWriter) error - // WriteContentType write content-type to http response writer. - WriteContentType(w http.ResponseWriter) -} - -var ( - _ Render = JSON{} - _ Render = MapJSON{} - _ Render = XML{} - _ Render = String{} - _ Render = Redirect{} - _ Render = Data{} - _ Render = PB{} -) - -func writeContentType(w http.ResponseWriter, value []string) { - header := w.Header() - if val := header["Content-Type"]; len(val) == 0 { - header["Content-Type"] = value - } -} diff --git a/pkg/net/http/blademaster/render/render.pb.go b/pkg/net/http/blademaster/render/render.pb.go deleted file mode 100644 index bb5390e98..000000000 --- a/pkg/net/http/blademaster/render/render.pb.go +++ /dev/null @@ -1,89 +0,0 @@ -// Code generated by protoc-gen-gogo. DO NOT EDIT. -// source: pb.proto - -/* -Package render is a generated protocol buffer package. - -It is generated from these files: - pb.proto - -It has these top-level messages: - PB -*/ -package render - -import proto "github.com/gogo/protobuf/proto" -import fmt "fmt" -import math "math" -import google_protobuf "github.com/gogo/protobuf/types" - -// Reference imports to suppress errors if they are not otherwise used. -var _ = proto.Marshal -var _ = fmt.Errorf -var _ = math.Inf - -// This is a compile-time assertion to ensure that this generated file -// is compatible with the proto package it is being compiled against. -// A compilation error at this line likely means your copy of the -// proto package needs to be updated. -const _ = proto.GoGoProtoPackageIsVersion2 // please upgrade the proto package - -type PB struct { - Code int64 `protobuf:"varint,1,opt,name=Code,proto3" json:"Code,omitempty"` - Message string `protobuf:"bytes,2,opt,name=Message,proto3" json:"Message,omitempty"` - TTL uint64 `protobuf:"varint,3,opt,name=TTL,proto3" json:"TTL,omitempty"` - Data *google_protobuf.Any `protobuf:"bytes,4,opt,name=Data" json:"Data,omitempty"` -} - -func (m *PB) Reset() { *m = PB{} } -func (m *PB) String() string { return proto.CompactTextString(m) } -func (*PB) ProtoMessage() {} -func (*PB) Descriptor() ([]byte, []int) { return fileDescriptorPb, []int{0} } - -func (m *PB) GetCode() int64 { - if m != nil { - return m.Code - } - return 0 -} - -func (m *PB) GetMessage() string { - if m != nil { - return m.Message - } - return "" -} - -func (m *PB) GetTTL() uint64 { - if m != nil { - return m.TTL - } - return 0 -} - -func (m *PB) GetData() *google_protobuf.Any { - if m != nil { - return m.Data - } - return nil -} - -func init() { - proto.RegisterType((*PB)(nil), "render.PB") -} - -func init() { proto.RegisterFile("pb.proto", fileDescriptorPb) } - -var fileDescriptorPb = []byte{ - // 154 bytes of a gzipped FileDescriptorProto - 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xe2, 0xe2, 0x28, 0x48, 0xd2, 0x2b, - 0x28, 0xca, 0x2f, 0xc9, 0x17, 0x62, 0x2b, 0x4a, 0xcd, 0x4b, 0x49, 0x2d, 0x92, 0x92, 0x4c, 0xcf, - 0xcf, 0x4f, 0xcf, 0x49, 0xd5, 0x07, 0x8b, 0x26, 0x95, 0xa6, 0xe9, 0x27, 0xe6, 0x55, 0x42, 0x94, - 0x28, 0xe5, 0x71, 0x31, 0x05, 0x38, 0x09, 0x09, 0x71, 0xb1, 0x38, 0xe7, 0xa7, 0xa4, 0x4a, 0x30, - 0x2a, 0x30, 0x6a, 0x30, 0x07, 0x81, 0xd9, 0x42, 0x12, 0x5c, 0xec, 0xbe, 0xa9, 0xc5, 0xc5, 0x89, - 0xe9, 0xa9, 0x12, 0x4c, 0x0a, 0x8c, 0x1a, 0x9c, 0x41, 0x30, 0xae, 0x90, 0x00, 0x17, 0x73, 0x48, - 0x88, 0x8f, 0x04, 0xb3, 0x02, 0xa3, 0x06, 0x4b, 0x10, 0x88, 0x29, 0xa4, 0xc1, 0xc5, 0xe2, 0x92, - 0x58, 0x92, 0x28, 0xc1, 0xa2, 0xc0, 0xa8, 0xc1, 0x6d, 0x24, 0xa2, 0x07, 0xb1, 0x4f, 0x0f, 0x66, - 0x9f, 0x9e, 0x63, 0x5e, 0x65, 0x10, 0x58, 0x45, 0x12, 0x1b, 0x58, 0xcc, 0x18, 0x10, 0x00, 0x00, - 0xff, 0xff, 0x7a, 0x92, 0x16, 0x71, 0xa5, 0x00, 0x00, 0x00, -} diff --git a/pkg/net/http/blademaster/render/render.proto b/pkg/net/http/blademaster/render/render.proto deleted file mode 100644 index e3f53870f..000000000 --- a/pkg/net/http/blademaster/render/render.proto +++ /dev/null @@ -1,14 +0,0 @@ -// use under command to generate pb.pb.go -// protoc --proto_path=.:$GOPATH/src/github.com/gogo/protobuf --gogo_out=Mgoogle/protobuf/any.proto=github.com/gogo/protobuf/types:. *.proto -syntax = "proto3"; -package render; - -import "google/protobuf/any.proto"; -import "github.com/gogo/protobuf/gogoproto/gogo.proto"; - -message PB { - int64 Code = 1; - string Message = 2; - uint64 TTL = 3; - google.protobuf.Any Data = 4; -} \ No newline at end of file diff --git a/pkg/net/http/blademaster/render/string.go b/pkg/net/http/blademaster/render/string.go deleted file mode 100644 index 4112b5713..000000000 --- a/pkg/net/http/blademaster/render/string.go +++ /dev/null @@ -1,40 +0,0 @@ -package render - -import ( - "fmt" - "io" - "net/http" - - "github.com/pkg/errors" -) - -var plainContentType = []string{"text/plain; charset=utf-8"} - -// String common string struct. -type String struct { - Format string - Data []interface{} -} - -// Render (String) writes data with custom ContentType. -func (r String) Render(w http.ResponseWriter) error { - return writeString(w, r.Format, r.Data) -} - -// WriteContentType writes string with text/plain ContentType. -func (r String) WriteContentType(w http.ResponseWriter) { - writeContentType(w, plainContentType) -} - -func writeString(w http.ResponseWriter, format string, data []interface{}) (err error) { - writeContentType(w, plainContentType) - if len(data) > 0 { - _, err = fmt.Fprintf(w, format, data...) - } else { - _, err = io.WriteString(w, format) - } - if err != nil { - err = errors.WithStack(err) - } - return -} diff --git a/pkg/net/http/blademaster/render/xml.go b/pkg/net/http/blademaster/render/xml.go deleted file mode 100644 index 8837c582c..000000000 --- a/pkg/net/http/blademaster/render/xml.go +++ /dev/null @@ -1,31 +0,0 @@ -package render - -import ( - "encoding/xml" - "net/http" - - "github.com/pkg/errors" -) - -// XML common xml struct. -type XML struct { - Code int - Message string - Data interface{} -} - -var xmlContentType = []string{"application/xml; charset=utf-8"} - -// Render (XML) writes data with xml ContentType. -func (r XML) Render(w http.ResponseWriter) (err error) { - r.WriteContentType(w) - if err = xml.NewEncoder(w).Encode(r.Data); err != nil { - err = errors.WithStack(err) - } - return -} - -// WriteContentType write xml ContentType. -func (r XML) WriteContentType(w http.ResponseWriter) { - writeContentType(w, xmlContentType) -} diff --git a/pkg/net/http/blademaster/routergroup.go b/pkg/net/http/blademaster/routergroup.go deleted file mode 100644 index 281796b12..000000000 --- a/pkg/net/http/blademaster/routergroup.go +++ /dev/null @@ -1,191 +0,0 @@ -package blademaster - -import ( - "regexp" -) - -// IRouter http router framework interface. -type IRouter interface { - IRoutes - Group(string, ...HandlerFunc) *RouterGroup -} - -// IRoutes http router interface. -type IRoutes interface { - UseFunc(...HandlerFunc) IRoutes - Use(...Handler) IRoutes - - Handle(string, string, ...HandlerFunc) IRoutes - HEAD(string, ...HandlerFunc) IRoutes - GET(string, ...HandlerFunc) IRoutes - POST(string, ...HandlerFunc) IRoutes - PUT(string, ...HandlerFunc) IRoutes - DELETE(string, ...HandlerFunc) IRoutes -} - -// RouterGroup is used internally to configure router, a RouterGroup is associated with a prefix -// and an array of handlers (middleware). -type RouterGroup struct { - Handlers []HandlerFunc - basePath string - engine *Engine - root bool - baseConfig *MethodConfig -} - -var _ IRouter = &RouterGroup{} - -// Use adds middleware to the group, see example code in doc. -func (group *RouterGroup) Use(middleware ...Handler) IRoutes { - for _, m := range middleware { - group.Handlers = append(group.Handlers, m.ServeHTTP) - } - return group.returnObj() -} - -// UseFunc adds middleware to the group, see example code in doc. -func (group *RouterGroup) UseFunc(middleware ...HandlerFunc) IRoutes { - group.Handlers = append(group.Handlers, middleware...) - return group.returnObj() -} - -// Group creates a new router group. You should add all the routes that have common middlwares or the same path prefix. -// For example, all the routes that use a common middlware for authorization could be grouped. -func (group *RouterGroup) Group(relativePath string, handlers ...HandlerFunc) *RouterGroup { - return &RouterGroup{ - Handlers: group.combineHandlers(handlers), - basePath: group.calculateAbsolutePath(relativePath), - engine: group.engine, - root: false, - } -} - -// SetMethodConfig is used to set config on specified method -func (group *RouterGroup) SetMethodConfig(config *MethodConfig) *RouterGroup { - group.baseConfig = config - return group -} - -// BasePath router group base path. -func (group *RouterGroup) BasePath() string { - return group.basePath -} - -func (group *RouterGroup) handle(httpMethod, relativePath string, handlers ...HandlerFunc) IRoutes { - absolutePath := group.calculateAbsolutePath(relativePath) - injections := group.injections(relativePath) - handlers = group.combineHandlers(injections, handlers) - group.engine.addRoute(httpMethod, absolutePath, handlers...) - if group.baseConfig != nil { - group.engine.SetMethodConfig(absolutePath, group.baseConfig) - } - return group.returnObj() -} - -// Handle registers a new request handle and middleware with the given path and method. -// The last handler should be the real handler, the other ones should be middleware that can and should be shared among different routes. -// See the example code in doc. -// -// For HEAD, GET, POST, PUT, and DELETE requests the respective shortcut -// functions can be used. -// -// This function is intended for bulk loading and to allow the usage of less -// frequently used, non-standardized or custom methods (e.g. for internal -// communication with a proxy). -func (group *RouterGroup) Handle(httpMethod, relativePath string, handlers ...HandlerFunc) IRoutes { - if matches, err := regexp.MatchString("^[A-Z]+$", httpMethod); !matches || err != nil { - panic("http method " + httpMethod + " is not valid") - } - return group.handle(httpMethod, relativePath, handlers...) -} - -// GET is a shortcut for router.Handle("GET", path, handle). -func (group *RouterGroup) GET(relativePath string, handlers ...HandlerFunc) IRoutes { - return group.handle("GET", relativePath, handlers...) -} - -// POST is a shortcut for router.Handle("POST", path, handle). -func (group *RouterGroup) POST(relativePath string, handlers ...HandlerFunc) IRoutes { - return group.handle("POST", relativePath, handlers...) -} - -// PUT is a shortcut for router.Handle("PUT", path, handle). -func (group *RouterGroup) PUT(relativePath string, handlers ...HandlerFunc) IRoutes { - return group.handle("PUT", relativePath, handlers...) -} - -// DELETE is a shortcut for router.Handle("DELETE", path, handle). -func (group *RouterGroup) DELETE(relativePath string, handlers ...HandlerFunc) IRoutes { - return group.handle("DELETE", relativePath, handlers...) -} - -// PATCH is a shortcut for router.Handle("PATCH", path, handle). -func (group *RouterGroup) PATCH(relativePath string, handlers ...HandlerFunc) IRoutes { - return group.handle("PATCH", relativePath, handlers...) -} - -// OPTIONS is a shortcut for router.Handle("OPTIONS", path, handle). -func (group *RouterGroup) OPTIONS(relativePath string, handlers ...HandlerFunc) IRoutes { - return group.handle("OPTIONS", relativePath, handlers...) -} - -// HEAD is a shortcut for router.Handle("HEAD", path, handle). -func (group *RouterGroup) HEAD(relativePath string, handlers ...HandlerFunc) IRoutes { - return group.handle("HEAD", relativePath, handlers...) -} - -func (group *RouterGroup) combineHandlers(handlerGroups ...[]HandlerFunc) []HandlerFunc { - finalSize := len(group.Handlers) - for _, handlers := range handlerGroups { - finalSize += len(handlers) - } - if finalSize >= int(_abortIndex) { - panic("too many handlers") - } - mergedHandlers := make([]HandlerFunc, finalSize) - copy(mergedHandlers, group.Handlers) - position := len(group.Handlers) - for _, handlers := range handlerGroups { - copy(mergedHandlers[position:], handlers) - position += len(handlers) - } - return mergedHandlers -} - -func (group *RouterGroup) calculateAbsolutePath(relativePath string) string { - return joinPaths(group.basePath, relativePath) -} - -func (group *RouterGroup) returnObj() IRoutes { - if group.root { - return group.engine - } - return group -} - -// injections is -func (group *RouterGroup) injections(relativePath string) []HandlerFunc { - absPath := group.calculateAbsolutePath(relativePath) - for _, injection := range group.engine.injections { - if !injection.pattern.MatchString(absPath) { - continue - } - return injection.handlers - } - return nil -} - -// Any registers a route that matches all the HTTP methods. -// GET, POST, PUT, PATCH, HEAD, OPTIONS, DELETE, CONNECT, TRACE. -func (group *RouterGroup) Any(relativePath string, handlers ...HandlerFunc) IRoutes { - group.handle("GET", relativePath, handlers...) - group.handle("POST", relativePath, handlers...) - group.handle("PUT", relativePath, handlers...) - group.handle("PATCH", relativePath, handlers...) - group.handle("HEAD", relativePath, handlers...) - group.handle("OPTIONS", relativePath, handlers...) - group.handle("DELETE", relativePath, handlers...) - group.handle("CONNECT", relativePath, handlers...) - group.handle("TRACE", relativePath, handlers...) - return group.returnObj() -} diff --git a/pkg/net/http/blademaster/server.go b/pkg/net/http/blademaster/server.go deleted file mode 100644 index 63187306a..000000000 --- a/pkg/net/http/blademaster/server.go +++ /dev/null @@ -1,517 +0,0 @@ -package blademaster - -import ( - "context" - "flag" - "fmt" - "net" - "net/http" - "os" - "regexp" - "strings" - "sync" - "sync/atomic" - "time" - - "github.com/go-kratos/kratos/pkg/conf/dsn" - "github.com/go-kratos/kratos/pkg/log" - "github.com/go-kratos/kratos/pkg/net/criticality" - "github.com/go-kratos/kratos/pkg/net/ip" - "github.com/go-kratos/kratos/pkg/net/metadata" - xtime "github.com/go-kratos/kratos/pkg/time" - - "github.com/pkg/errors" -) - -const ( - defaultMaxMemory = 32 << 20 // 32 MB -) - -var ( - _ IRouter = &Engine{} - - _httpDSN string - default405Body = []byte("405 method not allowed") - default404Body = []byte("404 page not found") -) - -func init() { - addFlag(flag.CommandLine) -} - -func addFlag(fs *flag.FlagSet) { - v := os.Getenv("HTTP") - if v == "" { - v = "tcp://0.0.0.0:8000/?timeout=1s" - } - fs.StringVar(&_httpDSN, "http", v, "listen http dsn, or use HTTP env variable.") -} - -func parseDSN(rawdsn string) *ServerConfig { - conf := new(ServerConfig) - d, err := dsn.Parse(rawdsn) - if err != nil { - panic(errors.Wrapf(err, "blademaster: invalid dsn: %s", rawdsn)) - } - if _, err = d.Bind(conf); err != nil { - panic(errors.Wrapf(err, "blademaster: invalid dsn: %s", rawdsn)) - } - return conf -} - -// Handler responds to an HTTP request. -type Handler interface { - ServeHTTP(c *Context) -} - -// HandlerFunc http request handler function. -type HandlerFunc func(*Context) - -// ServeHTTP calls f(ctx). -func (f HandlerFunc) ServeHTTP(c *Context) { - f(c) -} - -// ServerConfig is the bm server config model -type ServerConfig struct { - Network string `dsn:"network"` - Addr string `dsn:"address"` - Timeout xtime.Duration `dsn:"query.timeout"` - ReadTimeout xtime.Duration `dsn:"query.readTimeout"` - WriteTimeout xtime.Duration `dsn:"query.writeTimeout"` -} - -// MethodConfig is -type MethodConfig struct { - Timeout xtime.Duration -} - -// Start listen and serve bm engine by given DSN. -func (engine *Engine) Start() error { - conf := engine.conf - l, err := net.Listen(conf.Network, conf.Addr) - if err != nil { - return errors.Wrapf(err, "blademaster: listen tcp: %s", conf.Addr) - } - - log.Info("blademaster: start http listen addr: %s", l.Addr().String()) - server := &http.Server{ - ReadTimeout: time.Duration(conf.ReadTimeout), - WriteTimeout: time.Duration(conf.WriteTimeout), - } - go func() { - if err := engine.RunServer(server, l); err != nil { - if errors.Cause(err) == http.ErrServerClosed { - log.Info("blademaster: server closed") - return - } - panic(errors.Wrapf(err, "blademaster: engine.ListenServer(%+v, %+v)", server, l)) - } - }() - - return nil -} - -// Engine is the framework's instance, it contains the muxer, middleware and configuration settings. -// Create an instance of Engine, by using New() or Default() -type Engine struct { - RouterGroup - - lock sync.RWMutex - conf *ServerConfig - - address string - - trees methodTrees - server atomic.Value // store *http.Server - metastore map[string]map[string]interface{} // metastore is the path as key and the metadata of this path as value, it export via /metadata - - pcLock sync.RWMutex - methodConfigs map[string]*MethodConfig - - injections []injection - - // If enabled, the url.RawPath will be used to find parameters. - UseRawPath bool - - // If true, the path value will be unescaped. - // If UseRawPath is false (by default), the UnescapePathValues effectively is true, - // as url.Path gonna be used, which is already unescaped. - UnescapePathValues bool - - // If enabled, the router checks if another method is allowed for the - // current route, if the current request can not be routed. - // If this is the case, the request is answered with 'Method Not Allowed' - // and HTTP status code 405. - // If no other Method is allowed, the request is delegated to the NotFound - // handler. - HandleMethodNotAllowed bool - - allNoRoute []HandlerFunc - allNoMethod []HandlerFunc - noRoute []HandlerFunc - noMethod []HandlerFunc - - pool sync.Pool -} - -type injection struct { - pattern *regexp.Regexp - handlers []HandlerFunc -} - -// NewServer returns a new blank Engine instance without any middleware attached. -func NewServer(conf *ServerConfig) *Engine { - if conf == nil { - if !flag.Parsed() { - fmt.Fprint(os.Stderr, "[blademaster] please call flag.Parse() before Init blademaster server, some configure may not effect.\n") - } - conf = parseDSN(_httpDSN) - } - engine := &Engine{ - RouterGroup: RouterGroup{ - Handlers: nil, - basePath: "/", - root: true, - }, - address: ip.InternalIP(), - trees: make(methodTrees, 0, 9), - metastore: make(map[string]map[string]interface{}), - methodConfigs: make(map[string]*MethodConfig), - HandleMethodNotAllowed: true, - injections: make([]injection, 0), - } - if err := engine.SetConfig(conf); err != nil { - panic(err) - } - engine.pool.New = func() interface{} { - return engine.newContext() - } - engine.RouterGroup.engine = engine - // NOTE add prometheus monitor location - engine.addRoute("GET", "/metrics", monitor()) - engine.addRoute("GET", "/metadata", engine.metadata()) - engine.NoRoute(func(c *Context) { - c.Bytes(404, "text/plain", default404Body) - c.Abort() - }) - engine.NoMethod(func(c *Context) { - c.Bytes(405, "text/plain", []byte(http.StatusText(405))) - c.Abort() - }) - startPerf(engine) - return engine -} - -// SetMethodConfig is used to set config on specified path -func (engine *Engine) SetMethodConfig(path string, mc *MethodConfig) { - engine.pcLock.Lock() - engine.methodConfigs[path] = mc - engine.pcLock.Unlock() -} - -// DefaultServer returns an Engine instance with the Recovery and Logger middleware already attached. -func DefaultServer(conf *ServerConfig) *Engine { - engine := NewServer(conf) - engine.Use(Recovery(), Trace(), Logger()) - return engine -} - -func (engine *Engine) addRoute(method, path string, handlers ...HandlerFunc) { - if path[0] != '/' { - panic("blademaster: path must begin with '/'") - } - if method == "" { - panic("blademaster: HTTP method can not be empty") - } - if len(handlers) == 0 { - panic("blademaster: there must be at least one handler") - } - if _, ok := engine.metastore[path]; !ok { - engine.metastore[path] = make(map[string]interface{}) - } - engine.metastore[path]["method"] = method - root := engine.trees.get(method) - if root == nil { - root = new(node) - engine.trees = append(engine.trees, methodTree{method: method, root: root}) - } - - prelude := func(c *Context) { - c.method = method - c.RoutePath = path - } - handlers = append([]HandlerFunc{prelude}, handlers...) - root.addRoute(path, handlers) -} - -func (engine *Engine) prepareHandler(c *Context) { - httpMethod := c.Request.Method - rPath := c.Request.URL.Path - unescape := false - if engine.UseRawPath && len(c.Request.URL.EscapedPath()) > 0 { - rPath = c.Request.URL.EscapedPath() - unescape = engine.UnescapePathValues - } - rPath = cleanPath(rPath) - - // Find root of the tree for the given HTTP method - t := engine.trees - for i, tl := 0, len(t); i < tl; i++ { - if t[i].method != httpMethod { - continue - } - root := t[i].root - // Find route in tree - handlers, params, _ := root.getValue(rPath, c.Params, unescape) - if handlers != nil { - c.handlers = handlers - c.Params = params - return - } - break - } - - if engine.HandleMethodNotAllowed { - for _, tree := range engine.trees { - if tree.method == httpMethod { - continue - } - if handlers, _, _ := tree.root.getValue(rPath, nil, unescape); handlers != nil { - c.handlers = engine.allNoMethod - return - } - } - } - c.handlers = engine.allNoRoute - return -} - -func (engine *Engine) handleContext(c *Context) { - var cancel func() - req := c.Request - ctype := req.Header.Get("Content-Type") - switch { - case strings.Contains(ctype, "multipart/form-data"): - req.ParseMultipartForm(defaultMaxMemory) - default: - req.ParseForm() - } - // get derived timeout from http request header, - // compare with the engine configured, - // and use the minimum one - engine.lock.RLock() - tm := time.Duration(engine.conf.Timeout) - engine.lock.RUnlock() - // the method config is preferred - if pc := engine.methodConfig(c.Request.URL.Path); pc != nil { - tm = time.Duration(pc.Timeout) - } - if ctm := timeout(req); ctm > 0 && tm > ctm { - tm = ctm - } - md := metadata.MD{ - metadata.RemoteIP: remoteIP(req), - metadata.RemotePort: remotePort(req), - metadata.Criticality: string(criticality.Critical), - } - parseMetadataTo(req, md) - ctx := metadata.NewContext(context.Background(), md) - if tm > 0 { - c.Context, cancel = context.WithTimeout(ctx, tm) - } else { - c.Context, cancel = context.WithCancel(ctx) - } - defer cancel() - engine.prepareHandler(c) - c.Next() -} - -// SetConfig is used to set the engine configuration. -// Only the valid config will be loaded. -func (engine *Engine) SetConfig(conf *ServerConfig) (err error) { - if conf.Timeout <= 0 { - return errors.New("blademaster: config timeout must greater than 0") - } - if conf.Network == "" { - conf.Network = "tcp" - } - engine.lock.Lock() - engine.conf = conf - engine.lock.Unlock() - return -} - -func (engine *Engine) methodConfig(path string) *MethodConfig { - engine.pcLock.RLock() - mc := engine.methodConfigs[path] - engine.pcLock.RUnlock() - return mc -} - -// Router return a http.Handler for using http.ListenAndServe() directly. -func (engine *Engine) Router() http.Handler { - return engine -} - -// Server is used to load stored http server. -func (engine *Engine) Server() *http.Server { - s, ok := engine.server.Load().(*http.Server) - if !ok { - return nil - } - return s -} - -// Shutdown the http server without interrupting active connections. -func (engine *Engine) Shutdown(ctx context.Context) error { - server := engine.Server() - if server == nil { - return errors.New("blademaster: no server") - } - return errors.WithStack(server.Shutdown(ctx)) -} - -// UseFunc attachs a global middleware to the router. ie. the middleware attached though UseFunc() will be -// included in the handlers chain for every single request. Even 404, 405, static files... -// For example, this is the right place for a logger or error management middleware. -func (engine *Engine) UseFunc(middleware ...HandlerFunc) IRoutes { - engine.RouterGroup.UseFunc(middleware...) - engine.rebuild404Handlers() - engine.rebuild405Handlers() - return engine -} - -// Use attachs a global middleware to the router. ie. the middleware attached though Use() will be -// included in the handlers chain for every single request. Even 404, 405, static files... -// For example, this is the right place for a logger or error management middleware. -func (engine *Engine) Use(middleware ...Handler) IRoutes { - engine.RouterGroup.Use(middleware...) - engine.rebuild404Handlers() - engine.rebuild405Handlers() - return engine -} - -// Ping is used to set the general HTTP ping handler. -func (engine *Engine) Ping(handler HandlerFunc) { - engine.GET("/ping", handler) -} - -// Register is used to export metadata to discovery. -func (engine *Engine) Register(handler HandlerFunc) { - engine.GET("/register", handler) -} - -// Run attaches the router to a http.Server and starts listening and serving HTTP requests. -// It is a shortcut for http.ListenAndServe(addr, router) -// Note: this method will block the calling goroutine indefinitely unless an error happens. -func (engine *Engine) Run(addr ...string) (err error) { - address := resolveAddress(addr) - server := &http.Server{ - Addr: address, - Handler: engine, - } - engine.server.Store(server) - if err = server.ListenAndServe(); err != nil { - err = errors.Wrapf(err, "addrs: %v", addr) - } - return -} - -// RunTLS attaches the router to a http.Server and starts listening and serving HTTPS (secure) requests. -// It is a shortcut for http.ListenAndServeTLS(addr, certFile, keyFile, router) -// Note: this method will block the calling goroutine indefinitely unless an error happens. -func (engine *Engine) RunTLS(addr, certFile, keyFile string) (err error) { - server := &http.Server{ - Addr: addr, - Handler: engine, - } - engine.server.Store(server) - if err = server.ListenAndServeTLS(certFile, keyFile); err != nil { - err = errors.Wrapf(err, "tls: %s/%s:%s", addr, certFile, keyFile) - } - return -} - -// RunUnix attaches the router to a http.Server and starts listening and serving HTTP requests -// through the specified unix socket (ie. a file). -// Note: this method will block the calling goroutine indefinitely unless an error happens. -func (engine *Engine) RunUnix(file string) (err error) { - os.Remove(file) - listener, err := net.Listen("unix", file) - if err != nil { - err = errors.Wrapf(err, "unix: %s", file) - return - } - defer listener.Close() - server := &http.Server{ - Handler: engine, - } - engine.server.Store(server) - if err = server.Serve(listener); err != nil { - err = errors.Wrapf(err, "unix: %s", file) - } - return -} - -// RunServer will serve and start listening HTTP requests by given server and listener. -// Note: this method will block the calling goroutine indefinitely unless an error happens. -func (engine *Engine) RunServer(server *http.Server, l net.Listener) (err error) { - server.Handler = engine - engine.server.Store(server) - if err = server.Serve(l); err != nil { - err = errors.Wrapf(err, "listen server: %+v/%+v", server, l) - return - } - return -} - -func (engine *Engine) metadata() HandlerFunc { - return func(c *Context) { - c.JSON(engine.metastore, nil) - } -} - -// Inject is -func (engine *Engine) Inject(pattern string, handlers ...HandlerFunc) { - engine.injections = append(engine.injections, injection{ - pattern: regexp.MustCompile(pattern), - handlers: handlers, - }) -} - -// ServeHTTP conforms to the http.Handler interface. -func (engine *Engine) ServeHTTP(w http.ResponseWriter, req *http.Request) { - c := engine.pool.Get().(*Context) - c.Request = req - c.Writer = w - c.reset() - - engine.handleContext(c) - engine.pool.Put(c) -} - -//newContext for sync.pool -func (engine *Engine) newContext() *Context { - return &Context{engine: engine} -} - -// NoRoute adds handlers for NoRoute. It return a 404 code by default. -func (engine *Engine) NoRoute(handlers ...HandlerFunc) { - engine.noRoute = handlers - engine.rebuild404Handlers() -} - -// NoMethod sets the handlers called when... TODO. -func (engine *Engine) NoMethod(handlers ...HandlerFunc) { - engine.noMethod = handlers - engine.rebuild405Handlers() -} - -func (engine *Engine) rebuild404Handlers() { - engine.allNoRoute = engine.combineHandlers(engine.noRoute) -} - -func (engine *Engine) rebuild405Handlers() { - engine.allNoMethod = engine.combineHandlers(engine.noMethod) -} diff --git a/pkg/net/http/blademaster/server_test.go b/pkg/net/http/blademaster/server_test.go deleted file mode 100644 index 60343a52d..000000000 --- a/pkg/net/http/blademaster/server_test.go +++ /dev/null @@ -1,138 +0,0 @@ -package blademaster - -import ( - "context" - "fmt" - "io/ioutil" - "net/http" - "sync" - "sync/atomic" - "testing" - "time" - - criticalityPkg "github.com/go-kratos/kratos/pkg/net/criticality" - "github.com/go-kratos/kratos/pkg/net/metadata" - xtime "github.com/go-kratos/kratos/pkg/time" - - "github.com/stretchr/testify/assert" -) - -var ( - sonce sync.Once - - curEngine atomic.Value -) - -func uri(base, path string) string { - return fmt.Sprintf("%s://%s%s", "http", base, path) -} - -func shutdown() { - if err := curEngine.Load().(*Engine).Shutdown(context.TODO()); err != nil { - panic(err) - } -} - -func setupHandler(engine *Engine) { - // set the global timeout is 2 second - engine.conf.Timeout = xtime.Duration(time.Second * 2) - - engine.Ping(func(ctx *Context) { - ctx.AbortWithStatus(200) - }) - - engine.GET("/criticality/api", Criticality(criticalityPkg.Critical), func(ctx *Context) { - ctx.String(200, "%s", metadata.String(ctx, metadata.Criticality)) - }) - engine.GET("/criticality/none/api", func(ctx *Context) { - ctx.String(200, "%s", metadata.String(ctx, metadata.Criticality)) - }) -} - -func startServer(addr string) { - e := DefaultServer(nil) - setupHandler(e) - go e.Run(addr) - curEngine.Store(e) - time.Sleep(time.Second) -} - -func TestCriticality(t *testing.T) { - addr := "localhost:18001" - startServer(addr) - defer shutdown() - - tests := []*struct { - path string - crtl criticalityPkg.Criticality - expected criticalityPkg.Criticality - }{ - { - "/criticality/api", - criticalityPkg.EmptyCriticality, - criticalityPkg.Critical, - }, - { - "/criticality/api", - criticalityPkg.CriticalPlus, - criticalityPkg.Critical, - }, - { - "/criticality/api", - criticalityPkg.SheddablePlus, - criticalityPkg.Critical, - }, - } - client := &http.Client{} - for _, testCase := range tests { - req, err := http.NewRequest("GET", uri(addr, testCase.path), nil) - assert.NoError(t, err) - req.Header.Set("x-bm-metadata-criticality", string(testCase.crtl)) - resp, err := client.Do(req) - assert.NoError(t, err) - defer resp.Body.Close() - body, err := ioutil.ReadAll(resp.Body) - assert.NoError(t, err) - assert.Equal(t, testCase.expected, criticalityPkg.Criticality(body)) - } -} - -func TestNoneCriticality(t *testing.T) { - addr := "localhost:18002" - startServer(addr) - defer shutdown() - - tests := []*struct { - path string - crtl criticalityPkg.Criticality - expected criticalityPkg.Criticality - }{ - { - "/criticality/none/api", - criticalityPkg.EmptyCriticality, - criticalityPkg.Critical, - }, - { - "/criticality/none/api", - criticalityPkg.CriticalPlus, - criticalityPkg.CriticalPlus, - }, - { - "/criticality/none/api", - criticalityPkg.SheddablePlus, - criticalityPkg.SheddablePlus, - }, - } - client := &http.Client{} - for _, testCase := range tests { - req, err := http.NewRequest("GET", uri(addr, testCase.path), nil) - assert.NoError(t, err) - req.Header.Set("x-bm-metadata-criticality", string(testCase.crtl)) - resp, err := client.Do(req) - assert.NoError(t, err) - defer resp.Body.Close() - body, err := ioutil.ReadAll(resp.Body) - assert.NoError(t, err) - assert.Equal(t, testCase.expected, criticalityPkg.Criticality(body)) - } -} diff --git a/pkg/net/http/blademaster/trace.go b/pkg/net/http/blademaster/trace.go deleted file mode 100644 index b4625dadb..000000000 --- a/pkg/net/http/blademaster/trace.go +++ /dev/null @@ -1,229 +0,0 @@ -package blademaster - -import ( - "io" - "net/http" - "net/http/httptrace" - "strconv" - - "github.com/go-kratos/kratos/pkg/net/metadata" - "github.com/go-kratos/kratos/pkg/net/trace" -) - -const _defaultComponentName = "net/http" - -// Trace is trace middleware -func Trace() HandlerFunc { - return func(c *Context) { - // handle http request - // get derived trace from http request header - t, err := trace.Extract(trace.HTTPFormat, c.Request.Header) - if err != nil { - var opts []trace.Option - if ok, _ := strconv.ParseBool(trace.KratosTraceDebug); ok { - opts = append(opts, trace.EnableDebug()) - } - t = trace.New(c.Request.URL.Path, opts...) - } - t.SetTitle(c.Request.URL.Path) - t.SetTag(trace.String(trace.TagComponent, _defaultComponentName)) - t.SetTag(trace.String(trace.TagHTTPMethod, c.Request.Method)) - t.SetTag(trace.String(trace.TagHTTPURL, c.Request.URL.String())) - t.SetTag(trace.String(trace.TagSpanKind, "server")) - // business tag - t.SetTag(trace.String("caller", metadata.String(c.Context, metadata.Caller))) - // export trace id to user. - c.Writer.Header().Set(trace.KratosTraceID, t.TraceID()) - c.Context = trace.NewContext(c.Context, t) - c.Next() - t.Finish(&c.Error) - } -} - -type closeTracker struct { - io.ReadCloser - tr trace.Trace -} - -func (c *closeTracker) Close() error { - err := c.ReadCloser.Close() - c.tr.SetLog(trace.Log(trace.LogEvent, "ClosedBody")) - c.tr.Finish(&err) - return err -} - -// NewTraceTransport NewTraceTransport -func NewTraceTransport(rt http.RoundTripper, peerService string, internalTags ...trace.Tag) *TraceTransport { - return &TraceTransport{RoundTripper: rt, peerService: peerService, internalTags: internalTags} -} - -// TraceTransport wraps a RoundTripper. If a request is being traced with -// Tracer, Transport will inject the current span into the headers, -// and set HTTP related tags on the span. -type TraceTransport struct { - peerService string - internalTags []trace.Tag - // The actual RoundTripper to use for the request. A nil - // RoundTripper defaults to http.DefaultTransport. - http.RoundTripper -} - -// RoundTrip implements the RoundTripper interface -func (t *TraceTransport) RoundTrip(req *http.Request) (*http.Response, error) { - rt := t.RoundTripper - if rt == nil { - rt = http.DefaultTransport - } - tr, ok := trace.FromContext(req.Context()) - if !ok { - return rt.RoundTrip(req) - } - operationName := "HTTP:" + req.Method - // fork new trace - tr = tr.Fork("", operationName) - - tr.SetTag(trace.TagString(trace.TagComponent, _defaultComponentName)) - tr.SetTag(trace.TagString(trace.TagHTTPMethod, req.Method)) - tr.SetTag(trace.TagString(trace.TagHTTPURL, req.URL.String())) - tr.SetTag(trace.TagString(trace.TagSpanKind, "client")) - if t.peerService != "" { - tr.SetTag(trace.TagString(trace.TagPeerService, t.peerService)) - } - tr.SetTag(t.internalTags...) - - // inject trace to http header - trace.Inject(tr, trace.HTTPFormat, req.Header) - - // FIXME: uncomment after trace sdk is goroutinue safe - // ct := clientTracer{tr: tr} - // req = req.WithContext(httptrace.WithClientTrace(req.Context(), ct.clientTrace())) - resp, err := rt.RoundTrip(req) - - if err != nil { - tr.SetTag(trace.TagBool(trace.TagError, true)) - tr.Finish(&err) - return resp, err - } - - // TODO: get ecode - tr.SetTag(trace.TagInt64(trace.TagHTTPStatusCode, int64(resp.StatusCode))) - - if req.Method == "HEAD" { - tr.Finish(nil) - } else { - resp.Body = &closeTracker{resp.Body, tr} - } - return resp, err -} - -type clientTracer struct { - tr trace.Trace -} - -func (h *clientTracer) clientTrace() *httptrace.ClientTrace { - return &httptrace.ClientTrace{ - GetConn: h.getConn, - GotConn: h.gotConn, - PutIdleConn: h.putIdleConn, - GotFirstResponseByte: h.gotFirstResponseByte, - Got100Continue: h.got100Continue, - DNSStart: h.dnsStart, - DNSDone: h.dnsDone, - ConnectStart: h.connectStart, - ConnectDone: h.connectDone, - WroteHeaders: h.wroteHeaders, - Wait100Continue: h.wait100Continue, - WroteRequest: h.wroteRequest, - } -} - -func (h *clientTracer) getConn(hostPort string) { - // ext.HTTPUrl.Set(h.sp, hostPort) - h.tr.SetLog(trace.Log(trace.LogEvent, "GetConn")) -} - -func (h *clientTracer) gotConn(info httptrace.GotConnInfo) { - h.tr.SetTag(trace.TagBool("net/http.reused", info.Reused)) - h.tr.SetTag(trace.TagBool("net/http.was_idle", info.WasIdle)) - h.tr.SetLog(trace.Log(trace.LogEvent, "GotConn")) -} - -func (h *clientTracer) putIdleConn(error) { - h.tr.SetLog(trace.Log(trace.LogEvent, "PutIdleConn")) -} - -func (h *clientTracer) gotFirstResponseByte() { - h.tr.SetLog(trace.Log(trace.LogEvent, "GotFirstResponseByte")) -} - -func (h *clientTracer) got100Continue() { - h.tr.SetLog(trace.Log(trace.LogEvent, "Got100Continue")) -} - -func (h *clientTracer) dnsStart(info httptrace.DNSStartInfo) { - h.tr.SetLog( - trace.Log(trace.LogEvent, "DNSStart"), - trace.Log("host", info.Host), - ) -} - -func (h *clientTracer) dnsDone(info httptrace.DNSDoneInfo) { - fields := []trace.LogField{trace.Log(trace.LogEvent, "DNSDone")} - for _, addr := range info.Addrs { - fields = append(fields, trace.Log("addr", addr.String())) - } - if info.Err != nil { - // TODO: support log error object - fields = append(fields, trace.Log(trace.LogErrorObject, info.Err.Error())) - } - h.tr.SetLog(fields...) -} - -func (h *clientTracer) connectStart(network, addr string) { - h.tr.SetLog( - trace.Log(trace.LogEvent, "ConnectStart"), - trace.Log("network", network), - trace.Log("addr", addr), - ) -} - -func (h *clientTracer) connectDone(network, addr string, err error) { - if err != nil { - h.tr.SetLog( - trace.Log("message", "ConnectDone"), - trace.Log("network", network), - trace.Log("addr", addr), - trace.Log(trace.LogEvent, "error"), - // TODO: support log error object - trace.Log(trace.LogErrorObject, err.Error()), - ) - } else { - h.tr.SetLog( - trace.Log(trace.LogEvent, "ConnectDone"), - trace.Log("network", network), - trace.Log("addr", addr), - ) - } -} - -func (h *clientTracer) wroteHeaders() { - h.tr.SetLog(trace.Log("event", "WroteHeaders")) -} - -func (h *clientTracer) wait100Continue() { - h.tr.SetLog(trace.Log("event", "Wait100Continue")) -} - -func (h *clientTracer) wroteRequest(info httptrace.WroteRequestInfo) { - if info.Err != nil { - h.tr.SetLog( - trace.Log("message", "WroteRequest"), - trace.Log("event", "error"), - // TODO: support log error object - trace.Log(trace.LogErrorObject, info.Err.Error()), - ) - h.tr.SetTag(trace.TagBool(trace.TagError, true)) - } else { - h.tr.SetLog(trace.Log("event", "WroteRequest")) - } -} diff --git a/pkg/net/http/blademaster/tree.go b/pkg/net/http/blademaster/tree.go deleted file mode 100644 index 4b81a9101..000000000 --- a/pkg/net/http/blademaster/tree.go +++ /dev/null @@ -1,625 +0,0 @@ -// Copyright 2013 Julien Schmidt. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be found -// at https://github.com/julienschmidt/httprouter/blob/master/LICENSE - -package blademaster - -import ( - "net/url" - "strings" - "unicode" -) - -// Param is a single URL parameter, consisting of a key and a value. -type Param struct { - Key string - Value string -} - -// Params is a Param-slice, as returned by the router. -// The slice is ordered, the first URL parameter is also the first slice value. -// It is therefore safe to read values by the index. -type Params []Param - -// Get returns the value of the first Param which key matches the given name. -// If no matching Param is found, an empty string is returned. -func (ps Params) Get(name string) (string, bool) { - for _, entry := range ps { - if entry.Key == name { - return entry.Value, true - } - } - return "", false -} - -// ByName returns the value of the first Param which key matches the given name. -// If no matching Param is found, an empty string is returned. -func (ps Params) ByName(name string) (va string) { - va, _ = ps.Get(name) - return -} - -type methodTree struct { - method string - root *node -} - -type methodTrees []methodTree - -func (trees methodTrees) get(method string) *node { - for _, tree := range trees { - if tree.method == method { - return tree.root - } - } - return nil -} - -func min(a, b int) int { - if a <= b { - return a - } - return b -} - -func countParams(path string) uint8 { - var n uint - for i := 0; i < len(path); i++ { - if path[i] != ':' && path[i] != '*' { - continue - } - n++ - } - if n >= 255 { - return 255 - } - return uint8(n) -} - -type nodeType uint8 - -const ( - static nodeType = iota // default - root - param - catchAll -) - -type node struct { - path string - indices string - children []*node - handlers []HandlerFunc - priority uint32 - nType nodeType - maxParams uint8 - wildChild bool -} - -// increments priority of the given child and reorders if necessary. -func (n *node) incrementChildPrio(pos int) int { - n.children[pos].priority++ - prio := n.children[pos].priority - - // adjust position (move to front) - newPos := pos - for newPos > 0 && n.children[newPos-1].priority < prio { - // swap node positions - n.children[newPos-1], n.children[newPos] = n.children[newPos], n.children[newPos-1] - - newPos-- - } - - // build new index char string - if newPos != pos { - n.indices = n.indices[:newPos] + // unchanged prefix, might be empty - n.indices[pos:pos+1] + // the index char we move - n.indices[newPos:pos] + n.indices[pos+1:] // rest without char at 'pos' - } - - return newPos -} - -// addRoute adds a node with the given handle to the path. -// Not concurrency-safe! -func (n *node) addRoute(path string, handlers []HandlerFunc) { - fullPath := path - n.priority++ - numParams := countParams(path) - - // non-empty tree - if len(n.path) > 0 || len(n.children) > 0 { - walk: - for { - // Update maxParams of the current node - if numParams > n.maxParams { - n.maxParams = numParams - } - - // Find the longest common prefix. - // This also implies that the common prefix contains no ':' or '*' - // since the existing key can't contain those chars. - i := 0 - max := min(len(path), len(n.path)) - for i < max && path[i] == n.path[i] { - i++ - } - - // Split edge - if i < len(n.path) { - child := node{ - path: n.path[i:], - wildChild: n.wildChild, - indices: n.indices, - children: n.children, - handlers: n.handlers, - priority: n.priority - 1, - } - - // Update maxParams (max of all children) - for i := range child.children { - if child.children[i].maxParams > child.maxParams { - child.maxParams = child.children[i].maxParams - } - } - - n.children = []*node{&child} - // []byte for proper unicode char conversion, see #65 - n.indices = string([]byte{n.path[i]}) - n.path = path[:i] - n.handlers = nil - n.wildChild = false - } - - // Make new node a child of this node - if i < len(path) { - path = path[i:] - - if n.wildChild { - n = n.children[0] - n.priority++ - - // Update maxParams of the child node - if numParams > n.maxParams { - n.maxParams = numParams - } - numParams-- - - // Check if the wildcard matches - if len(path) >= len(n.path) && n.path == path[:len(n.path)] { - // check for longer wildcard, e.g. :name and :names - if len(n.path) >= len(path) || path[len(n.path)] == '/' { - continue walk - } - } - - pathSeg := path - if n.nType != catchAll { - pathSeg = strings.SplitN(path, "/", 2)[0] - } - prefix := fullPath[:strings.Index(fullPath, pathSeg)] + n.path - panic("'" + pathSeg + - "' in new path '" + fullPath + - "' conflicts with existing wildcard '" + n.path + - "' in existing prefix '" + prefix + - "'") - } - - c := path[0] - - // slash after param - if n.nType == param && c == '/' && len(n.children) == 1 { - n = n.children[0] - n.priority++ - continue walk - } - - // Check if a child with the next path byte exists - for i := 0; i < len(n.indices); i++ { - if c == n.indices[i] { - i = n.incrementChildPrio(i) - n = n.children[i] - continue walk - } - } - - // Otherwise insert it - if c != ':' && c != '*' { - // []byte for proper unicode char conversion, see #65 - n.indices += string([]byte{c}) - child := &node{ - maxParams: numParams, - } - n.children = append(n.children, child) - n.incrementChildPrio(len(n.indices) - 1) - n = child - } - n.insertChild(numParams, path, fullPath, handlers) - return - } else if i == len(path) { // Make node a (in-path) leaf - if n.handlers != nil { - panic("handlers are already registered for path '" + fullPath + "'") - } - n.handlers = handlers - } - return - } - } else { // Empty tree - n.insertChild(numParams, path, fullPath, handlers) - n.nType = root - } -} - -func (n *node) insertChild(numParams uint8, path string, fullPath string, handlers []HandlerFunc) { - var offset int // already handled bytes of the path - - // find prefix until first wildcard (beginning with ':' or '*') - for i, max := 0, len(path); numParams > 0; i++ { - c := path[i] - if c != ':' && c != '*' { - continue - } - - // find wildcard end (either '/' or path end) - end := i + 1 - for end < max && path[end] != '/' { - switch path[end] { - // the wildcard name must not contain ':' and '*' - case ':', '*': - panic("only one wildcard per path segment is allowed, has: '" + - path[i:] + "' in path '" + fullPath + "'") - default: - end++ - } - } - - // check if this Node existing children which would be - // unreachable if we insert the wildcard here - if len(n.children) > 0 { - panic("wildcard route '" + path[i:end] + - "' conflicts with existing children in path '" + fullPath + "'") - } - - // check if the wildcard has a name - if end-i < 2 { - panic("wildcards must be named with a non-empty name in path '" + fullPath + "'") - } - - if c == ':' { // param - // split path at the beginning of the wildcard - if i > 0 { - n.path = path[offset:i] - offset = i - } - - child := &node{ - nType: param, - maxParams: numParams, - } - n.children = []*node{child} - n.wildChild = true - n = child - n.priority++ - numParams-- - - // if the path doesn't end with the wildcard, then there - // will be another non-wildcard subpath starting with '/' - if end < max { - n.path = path[offset:end] - offset = end - - child := &node{ - maxParams: numParams, - priority: 1, - } - n.children = []*node{child} - n = child - } - } else { // catchAll - if end != max || numParams > 1 { - panic("catch-all routes are only allowed at the end of the path in path '" + fullPath + "'") - } - - if len(n.path) > 0 && n.path[len(n.path)-1] == '/' { - panic("catch-all conflicts with existing handle for the path segment root in path '" + fullPath + "'") - } - - // currently fixed width 1 for '/' - i-- - if path[i] != '/' { - panic("no / before catch-all in path '" + fullPath + "'") - } - - n.path = path[offset:i] - - // first node: catchAll node with empty path - child := &node{ - wildChild: true, - nType: catchAll, - maxParams: 1, - } - n.children = []*node{child} - n.indices = string(path[i]) - n = child - n.priority++ - - // second node: node holding the variable - child = &node{ - path: path[i:], - nType: catchAll, - maxParams: 1, - handlers: handlers, - priority: 1, - } - n.children = []*node{child} - - return - } - } - - // insert remaining path part and handle to the leaf - n.path = path[offset:] - n.handlers = handlers -} - -// getValue returns the handle registered with the given path (key). The values of -// wildcards are saved to a map. -// If no handle can be found, a TSR (trailing slash redirect) recommendation is -// made if a handle exists with an extra (without the) trailing slash for the -// given path. -func (n *node) getValue(path string, po Params, unescape bool) (handlers []HandlerFunc, p Params, tsr bool) { - p = po -walk: // Outer loop for walking the tree - for { - if len(path) > len(n.path) { - if path[:len(n.path)] == n.path { - path = path[len(n.path):] - // If this node does not have a wildcard (param or catchAll) - // child, we can just look up the next child node and continue - // to walk down the tree - if !n.wildChild { - c := path[0] - for i := 0; i < len(n.indices); i++ { - if c == n.indices[i] { - n = n.children[i] - continue walk - } - } - - // Nothing found. - // We can recommend to redirect to the same URL without a - // trailing slash if a leaf exists for that path. - tsr = path == "/" && n.handlers != nil - return - } - - // handle wildcard child - n = n.children[0] - switch n.nType { - case param: - // find param end (either '/' or path end) - end := 0 - for end < len(path) && path[end] != '/' { - end++ - } - - // save param value - if cap(p) < int(n.maxParams) { - p = make(Params, 0, n.maxParams) - } - i := len(p) - p = p[:i+1] // expand slice within preallocated capacity - p[i].Key = n.path[1:] - val := path[:end] - if unescape { - var err error - if p[i].Value, err = url.QueryUnescape(val); err != nil { - p[i].Value = val // fallback, in case of error - } - } else { - p[i].Value = val - } - - // we need to go deeper! - if end < len(path) { - if len(n.children) > 0 { - path = path[end:] - n = n.children[0] - continue walk - } - - // ... but we can't - tsr = len(path) == end+1 - return - } - - if handlers = n.handlers; handlers != nil { - return - } - if len(n.children) == 1 { - // No handle found. Check if a handle for this path + a - // trailing slash exists for TSR recommendation - n = n.children[0] - tsr = n.path == "/" && n.handlers != nil - } - - return - - case catchAll: - // save param value - if cap(p) < int(n.maxParams) { - p = make(Params, 0, n.maxParams) - } - i := len(p) - p = p[:i+1] // expand slice within preallocated capacity - p[i].Key = n.path[2:] - if unescape { - var err error - if p[i].Value, err = url.QueryUnescape(path); err != nil { - p[i].Value = path // fallback, in case of error - } - } else { - p[i].Value = path - } - - handlers = n.handlers - return - - default: - panic("invalid node type") - } - } - } else if path == n.path { - // We should have reached the node containing the handle. - // Check if this node has a handle registered. - if handlers = n.handlers; handlers != nil { - return - } - - if path == "/" && n.wildChild && n.nType != root { - tsr = true - return - } - - // No handle found. Check if a handle for this path + a - // trailing slash exists for trailing slash recommendation - for i := 0; i < len(n.indices); i++ { - if n.indices[i] == '/' { - n = n.children[i] - tsr = (len(n.path) == 1 && n.handlers != nil) || - (n.nType == catchAll && n.children[0].handlers != nil) - return - } - } - - return - } - - // Nothing found. We can recommend to redirect to the same URL with an - // extra trailing slash if a leaf exists for that path - tsr = (path == "/") || - (len(n.path) == len(path)+1 && n.path[len(path)] == '/' && - path == n.path[:len(n.path)-1] && n.handlers != nil) - return - } -} - -// findCaseInsensitivePath makes a case-insensitive lookup of the given path and tries to find a handler. -// It can optionally also fix trailing slashes. -// It returns the case-corrected path and a bool indicating whether the lookup -// was successful. -func (n *node) findCaseInsensitivePath(path string, fixTrailingSlash bool) (ciPath []byte, found bool) { - ciPath = make([]byte, 0, len(path)+1) // preallocate enough memory - - // Outer loop for walking the tree - for len(path) >= len(n.path) && strings.EqualFold(path[:len(n.path)], n.path) { - path = path[len(n.path):] - ciPath = append(ciPath, n.path...) - - if len(path) > 0 { - // If this node does not have a wildcard (param or catchAll) child, - // we can just look up the next child node and continue to walk down - // the tree - if !n.wildChild { - r := unicode.ToLower(rune(path[0])) - for i, index := range n.indices { - // must use recursive approach since both index and - // ToLower(index) could exist. We must check both. - if r == unicode.ToLower(index) { - out, _found := n.children[i].findCaseInsensitivePath(path, fixTrailingSlash) - if _found { - return append(ciPath, out...), true - } - } - } - - // Nothing found. We can recommend to redirect to the same URL - // without a trailing slash if a leaf exists for that path - found = fixTrailingSlash && path == "/" && n.handlers != nil - return - } - - n = n.children[0] - switch n.nType { - case param: - // find param end (either '/' or path end) - k := 0 - for k < len(path) && path[k] != '/' { - k++ - } - - // add param value to case insensitive path - ciPath = append(ciPath, path[:k]...) - - // we need to go deeper! - if k < len(path) { - if len(n.children) > 0 { - path = path[k:] - n = n.children[0] - continue - } - - // ... but we can't - if fixTrailingSlash && len(path) == k+1 { - return ciPath, true - } - return - } - - if n.handlers != nil { - return ciPath, true - } else if fixTrailingSlash && len(n.children) == 1 { - // No handle found. Check if a handle for this path + a - // trailing slash exists - n = n.children[0] - if n.path == "/" && n.handlers != nil { - return append(ciPath, '/'), true - } - } - return - - case catchAll: - return append(ciPath, path...), true - - default: - panic("invalid node type") - } - } else { - // We should have reached the node containing the handle. - // Check if this node has a handle registered. - if n.handlers != nil { - return ciPath, true - } - - // No handle found. - // Try to fix the path by adding a trailing slash - if fixTrailingSlash { - for i := 0; i < len(n.indices); i++ { - if n.indices[i] == '/' { - n = n.children[i] - if (len(n.path) == 1 && n.handlers != nil) || - (n.nType == catchAll && n.children[0].handlers != nil) { - return append(ciPath, '/'), true - } - return - } - } - } - return - } - } - - // Nothing found. - // Try to fix the path by adding / removing a trailing slash - if fixTrailingSlash { - if path == "/" { - return ciPath, true - } - if len(path)+1 == len(n.path) && n.path[len(path)] == '/' && - strings.EqualFold(path, n.path[:len(path)]) && - n.handlers != nil { - return append(ciPath, n.path...), true - } - } - return -} diff --git a/pkg/net/http/blademaster/utils.go b/pkg/net/http/blademaster/utils.go deleted file mode 100644 index 7bd86034f..000000000 --- a/pkg/net/http/blademaster/utils.go +++ /dev/null @@ -1,159 +0,0 @@ -package blademaster - -import ( - "os" - "path" -) - -func lastChar(str string) uint8 { - if str == "" { - panic("The length of the string can't be 0") - } - return str[len(str)-1] -} - -func joinPaths(absolutePath, relativePath string) string { - if relativePath == "" { - return absolutePath - } - - finalPath := path.Join(absolutePath, relativePath) - appendSlash := lastChar(relativePath) == '/' && lastChar(finalPath) != '/' - if appendSlash { - return finalPath + "/" - } - return finalPath -} - -func resolveAddress(addr []string) string { - switch len(addr) { - case 0: - if port := os.Getenv("PORT"); port != "" { - //debugPrint("Environment variable PORT=\"%s\"", port) - return ":" + port - } - //debugPrint("Environment variable PORT is undefined. Using port :8080 by default") - return ":8080" - case 1: - return addr[0] - default: - panic("too much parameters") - } -} - -// cleanPath is the URL version of path.Clean, it returns a canonical URL path -// for p, eliminating . and .. elements. -// -// The following rules are applied iteratively until no further processing can -// be done: -// 1. Replace multiple slashes with a single slash. -// 2. Eliminate each . path name element (the current directory). -// 3. Eliminate each inner .. path name element (the parent directory) -// along with the non-.. element that precedes it. -// 4. Eliminate .. elements that begin a rooted path: -// that is, replace "/.." by "/" at the beginning of a path. -// -// If the result of this process is an empty string, "/" is returned. -func cleanPath(p string) string { - // Turn empty string into "/" - if p == "" { - return "/" - } - - n := len(p) - var buf []byte - - // Invariants: - // reading from path; r is index of next byte to process. - // writing to buf; w is index of next byte to write. - - // path must start with '/' - r := 1 - w := 1 - - if p[0] != '/' { - r = 0 - buf = make([]byte, n+1) - buf[0] = '/' - } - - trailing := n > 1 && p[n-1] == '/' - - // A bit more clunky without a 'lazybuf' like the path package, but the loop - // gets completely inlined (bufApp). So in contrast to the path package this - // loop has no expensive function calls (except 1x make) - - for r < n { - switch { - case p[r] == '/': - // empty path element, trailing slash is added after the end - r++ - - case p[r] == '.' && r+1 == n: - trailing = true - r++ - - case p[r] == '.' && p[r+1] == '/': - // . element - r += 2 - - case p[r] == '.' && p[r+1] == '.' && (r+2 == n || p[r+2] == '/'): - // .. element: remove to last / - r += 3 - - if w > 1 { - // can backtrack - w-- - - if buf == nil { - for w > 1 && p[w] != '/' { - w-- - } - } else { - for w > 1 && buf[w] != '/' { - w-- - } - } - } - - default: - // real path element. - // add slash if needed - if w > 1 { - bufApp(&buf, p, w, '/') - w++ - } - - // copy element - for r < n && p[r] != '/' { - bufApp(&buf, p, w, p[r]) - w++ - r++ - } - } - } - - // re-append trailing slash - if trailing && w > 1 { - bufApp(&buf, p, w, '/') - w++ - } - - if buf == nil { - return p[:w] - } - return string(buf[:w]) -} - -// internal helper to lazily create a buffer if necessary. -func bufApp(buf *[]byte, s string, w int, c byte) { - if *buf == nil { - if s[w] == c { - return - } - - *buf = make([]byte, len(s)) - copy(*buf, s[:w]) - } - (*buf)[w] = c -} diff --git a/pkg/net/ip/ip.go b/pkg/net/ip/ip.go deleted file mode 100644 index 0966ccc5e..000000000 --- a/pkg/net/ip/ip.go +++ /dev/null @@ -1,74 +0,0 @@ -package ip - -import ( - "net" - "strings" -) - -// ExternalIP get external ip. -func ExternalIP() (res []string) { - inters, err := net.Interfaces() - if err != nil { - return - } - for _, inter := range inters { - if !strings.HasPrefix(inter.Name, "lo") { - addrs, err := inter.Addrs() - if err != nil { - continue - } - for _, addr := range addrs { - if ipnet, ok := addr.(*net.IPNet); ok { - if ipnet.IP.IsLoopback() || ipnet.IP.IsLinkLocalMulticast() || ipnet.IP.IsLinkLocalUnicast() { - continue - } - if ip4 := ipnet.IP.To4(); ip4 != nil { - switch true { - case ip4[0] == 10: - continue - case ip4[0] == 172 && ip4[1] >= 16 && ip4[1] <= 31: - continue - case ip4[0] == 192 && ip4[1] == 168: - continue - default: - res = append(res, ipnet.IP.String()) - } - } - } - } - } - } - return -} - -// InternalIP get internal ip. -func InternalIP() string { - inters, err := net.Interfaces() - if err != nil { - return "" - } - for _, inter := range inters { - if !isUp(inter.Flags) { - continue - } - if !strings.HasPrefix(inter.Name, "lo") { - addrs, err := inter.Addrs() - if err != nil { - continue - } - for _, addr := range addrs { - if ipnet, ok := addr.(*net.IPNet); ok && !ipnet.IP.IsLoopback() { - if ipnet.IP.To4() != nil { - return ipnet.IP.String() - } - } - } - } - } - return "" -} - -// isUp Interface is up -func isUp(v net.Flags) bool { - return v&net.FlagUp == net.FlagUp -} diff --git a/pkg/net/metadata/README.md b/pkg/net/metadata/README.md deleted file mode 100644 index eb51734b8..000000000 --- a/pkg/net/metadata/README.md +++ /dev/null @@ -1,5 +0,0 @@ -# net/metadata - -## 项目简介 - -用于储存各种元信息 diff --git a/pkg/net/metadata/key.go b/pkg/net/metadata/key.go deleted file mode 100644 index cde79d943..000000000 --- a/pkg/net/metadata/key.go +++ /dev/null @@ -1,67 +0,0 @@ -package metadata - -// metadata common key -const ( - - // Network - RemoteIP = "remote_ip" - RemotePort = "remote_port" - ServerAddr = "server_addr" - ClientAddr = "client_addr" - - // Router - Cluster = "cluster" - Color = "color" - - // Trace - Trace = "trace" - Caller = "caller" - - // Timeout - Timeout = "timeout" - - // Dispatch - CPUUsage = "cpu_usage" - Errors = "errors" - Requests = "requests" - - // Mirror - Mirror = "mirror" - - // Mid 外网账户用户id - Mid = "mid" // NOTE: !!!业务可重新修改key名!!! - - // Device 客户端信息 - Device = "device" - - // Criticality 重要性 - Criticality = "criticality" -) - -var outgoingKey = map[string]struct{}{ - Color: {}, - RemoteIP: {}, - RemotePort: {}, - Mirror: {}, - Criticality: {}, -} - -var incomingKey = map[string]struct{}{ - Caller: {}, -} - -// IsOutgoingKey represent this key should propagate by rpc. -func IsOutgoingKey(key string) bool { - _, ok := outgoingKey[key] - return ok -} - -// IsIncomingKey represent this key should extract from rpc metadata. -func IsIncomingKey(key string) (ok bool) { - _, ok = outgoingKey[key] - if ok { - return - } - _, ok = incomingKey[key] - return -} diff --git a/pkg/net/metadata/metadata.go b/pkg/net/metadata/metadata.go deleted file mode 100644 index 83eb3657c..000000000 --- a/pkg/net/metadata/metadata.go +++ /dev/null @@ -1,156 +0,0 @@ -package metadata - -import ( - "context" - "fmt" - "strconv" - - "github.com/pkg/errors" -) - -// MD is a mapping from metadata keys to values. -type MD map[string]interface{} - -type mdKey struct{} - -// Len returns the number of items in md. -func (md MD) Len() int { - return len(md) -} - -// Copy returns a copy of md. -func (md MD) Copy() MD { - return Join(md) -} - -// New creates an MD from a given key-value map. -func New(m map[string]interface{}) MD { - md := MD{} - for k, val := range m { - md[k] = val - } - return md -} - -// Join joins any number of mds into a single MD. -// The order of values for each key is determined by the order in which -// the mds containing those values are presented to Join. -func Join(mds ...MD) MD { - out := MD{} - for _, md := range mds { - for k, v := range md { - out[k] = v - } - } - return out -} - -// Pairs returns an MD formed by the mapping of key, value ... -// Pairs panics if len(kv) is odd. -func Pairs(kv ...interface{}) MD { - if len(kv)%2 == 1 { - panic(fmt.Sprintf("metadata: Pairs got the odd number of input pairs for metadata: %d", len(kv))) - } - md := MD{} - var key string - for i, s := range kv { - if i%2 == 0 { - key = s.(string) - continue - } - md[key] = s - } - return md -} - -// NewContext creates a new context with md attached. -func NewContext(ctx context.Context, md MD) context.Context { - return context.WithValue(ctx, mdKey{}, md) -} - -// FromContext returns the incoming metadata in ctx if it exists. The -// returned MD should not be modified. Writing to it may cause races. -// Modification should be made to copies of the returned MD. -func FromContext(ctx context.Context) (md MD, ok bool) { - md, ok = ctx.Value(mdKey{}).(MD) - return -} - -// String get string value from metadata in context -func String(ctx context.Context, key string) string { - md, ok := ctx.Value(mdKey{}).(MD) - if !ok { - return "" - } - str, _ := md[key].(string) - return str -} - -// Int64 get int64 value from metadata in context -func Int64(ctx context.Context, key string) int64 { - md, ok := ctx.Value(mdKey{}).(MD) - if !ok { - return 0 - } - i64, _ := md[key].(int64) - return i64 -} - -// Value get value from metadata in context return nil if not found -func Value(ctx context.Context, key string) interface{} { - md, ok := ctx.Value(mdKey{}).(MD) - if !ok { - return nil - } - return md[key] -} - -// WithContext return no deadline context and retain metadata. -func WithContext(c context.Context) context.Context { - md, ok := FromContext(c) - if ok { - nmd := md.Copy() - // NOTE: temporary delete prevent asynchronous task reuse finished task - delete(nmd, Trace) - return NewContext(context.Background(), nmd) - } - return context.Background() -} - -// Bool get boolean from metadata in context use strconv.Parse. -func Bool(ctx context.Context, key string) bool { - md, ok := ctx.Value(mdKey{}).(MD) - if !ok { - return false - } - - switch md[key].(type) { - case bool: - return md[key].(bool) - case string: - ok, _ = strconv.ParseBool(md[key].(string)) - return ok - default: - return false - } -} - -// Range range value from metadata in context filtered by filterFunc. -func Range(ctx context.Context, rangeFunc func(key string, value interface{}), filterFunc ...func(key string) bool) { - var filter func(key string) bool - filterLen := len(filterFunc) - if filterLen > 1 { - panic(errors.New("metadata: Range got the lenth of filterFunc must less than 2")) - } else if filterLen == 1 { - filter = filterFunc[0] - } - md, ok := ctx.Value(mdKey{}).(MD) - if !ok { - return - } - for key, value := range md { - if filter == nil || filter(key) { - rangeFunc(key, value) - } - } -} diff --git a/pkg/net/metadata/metadata_test.go b/pkg/net/metadata/metadata_test.go deleted file mode 100644 index 48a1fd2bc..000000000 --- a/pkg/net/metadata/metadata_test.go +++ /dev/null @@ -1,148 +0,0 @@ -package metadata - -import ( - "context" - "reflect" - "testing" - - "github.com/stretchr/testify/assert" -) - -func TestPairsMD(t *testing.T) { - for _, test := range []struct { - // input - kv []interface{} - // output - md MD - }{ - {[]interface{}{}, MD{}}, - {[]interface{}{"k1", "v1", "k1", "v2"}, MD{"k1": "v2"}}, - } { - md := Pairs(test.kv...) - if !reflect.DeepEqual(md, test.md) { - t.Fatalf("Pairs(%v) = %v, want %v", test.kv, md, test.md) - } - } -} -func TestCopy(t *testing.T) { - const key, val = "key", "val" - orig := Pairs(key, val) - copy := orig.Copy() - if !reflect.DeepEqual(orig, copy) { - t.Errorf("copied value not equal to the original, got %v, want %v", copy, orig) - } - orig[key] = "foo" - if v := copy[key]; v != val { - t.Errorf("change in original should not affect copy, got %q, want %q", v, val) - } -} -func TestJoin(t *testing.T) { - for _, test := range []struct { - mds []MD - want MD - }{ - {[]MD{}, MD{}}, - {[]MD{Pairs("foo", "bar")}, Pairs("foo", "bar")}, - {[]MD{Pairs("foo", "bar"), Pairs("foo", "baz")}, Pairs("foo", "bar", "foo", "baz")}, - {[]MD{Pairs("foo", "bar"), Pairs("foo", "baz"), Pairs("zip", "zap")}, Pairs("foo", "bar", "foo", "baz", "zip", "zap")}, - } { - md := Join(test.mds...) - if !reflect.DeepEqual(md, test.want) { - t.Errorf("context's metadata is %v, want %v", md, test.want) - } - } -} - -func TestWithContext(t *testing.T) { - md := MD(map[string]interface{}{RemoteIP: "127.0.0.1", Color: "red", Mirror: true}) - c := NewContext(context.Background(), md) - ctx := WithContext(c) - md1, ok := FromContext(ctx) - if !ok { - t.Errorf("expect ok be true") - t.FailNow() - } - if !reflect.DeepEqual(md1, md) { - t.Errorf("expect md1 equal to md") - t.FailNow() - } -} - -func TestBool(t *testing.T) { - md := MD{RemoteIP: "127.0.0.1", Color: "red"} - mdcontext := NewContext(context.Background(), md) - assert.Equal(t, false, Bool(mdcontext, Mirror)) - - mdcontext = NewContext(context.Background(), MD{Mirror: true}) - assert.Equal(t, true, Bool(mdcontext, Mirror)) - - mdcontext = NewContext(context.Background(), MD{Mirror: "true"}) - assert.Equal(t, true, Bool(mdcontext, Mirror)) - - mdcontext = NewContext(context.Background(), MD{Mirror: "1"}) - assert.Equal(t, true, Bool(mdcontext, Mirror)) - - mdcontext = NewContext(context.Background(), MD{Mirror: "0"}) - assert.Equal(t, false, Bool(mdcontext, Mirror)) -} -func TestInt64(t *testing.T) { - mdcontext := NewContext(context.Background(), MD{Mid: int64(1)}) - assert.Equal(t, int64(1), Int64(mdcontext, Mid)) - mdcontext = NewContext(context.Background(), MD{Mid: int64(2)}) - assert.NotEqual(t, int64(1), Int64(mdcontext, Mid)) - mdcontext = NewContext(context.Background(), MD{Mid: 10}) - assert.NotEqual(t, int64(10), Int64(mdcontext, Mid)) -} - -func TestRange(t *testing.T) { - for _, test := range []struct { - filterFunc func(key string) bool - md MD - want MD - }{ - { - nil, - Pairs("foo", "bar"), - Pairs("foo", "bar"), - }, - { - IsOutgoingKey, - Pairs("foo", "bar", RemoteIP, "127.0.0.1", Color, "red", Mirror, "false"), - Pairs(RemoteIP, "127.0.0.1", Color, "red", Mirror, "false"), - }, - { - IsOutgoingKey, - Pairs("foo", "bar", Caller, "app-feed", RemoteIP, "127.0.0.1", Color, "red", Mirror, "true"), - Pairs(RemoteIP, "127.0.0.1", Color, "red", Mirror, "true"), - }, - { - IsIncomingKey, - Pairs("foo", "bar", Caller, "app-feed", RemoteIP, "127.0.0.1", Color, "red", Mirror, "true"), - Pairs(Caller, "app-feed", RemoteIP, "127.0.0.1", Color, "red", Mirror, "true"), - }, - } { - var mds []MD - c := NewContext(context.Background(), test.md) - ctx := WithContext(c) - Range(ctx, - func(key string, value interface{}) { - mds = append(mds, Pairs(key, value)) - }, - test.filterFunc) - rmd := Join(mds...) - if !reflect.DeepEqual(rmd, test.want) { - t.Fatalf("Range(%v) = %v, want %v", test.md, rmd, test.want) - } - if test.filterFunc == nil { - var mds []MD - Range(ctx, - func(key string, value interface{}) { - mds = append(mds, Pairs(key, value)) - }) - rmd := Join(mds...) - if !reflect.DeepEqual(rmd, test.want) { - t.Fatalf("Range(%v) = %v, want %v", test.md, rmd, test.want) - } - } - } -} diff --git a/pkg/net/netutil/backoff.go b/pkg/net/netutil/backoff.go deleted file mode 100644 index d96f511f3..000000000 --- a/pkg/net/netutil/backoff.go +++ /dev/null @@ -1,72 +0,0 @@ -package netutil - -import ( - "math/rand" - "time" -) - -// DefaultBackoffConfig uses values specified for backoff in common. -var DefaultBackoffConfig = BackoffConfig{ - MaxDelay: 120 * time.Second, - BaseDelay: 1.0 * time.Second, - Factor: 1.6, - Jitter: 0.2, -} - -// Backoff defines the methodology for backing off after a call failure. -type Backoff interface { - // Backoff returns the amount of time to wait before the next retry given - // the number of consecutive failures. - Backoff(retries int) time.Duration -} - -// BackoffConfig defines the parameters for the default backoff strategy. -type BackoffConfig struct { - // MaxDelay is the upper bound of backoff delay. - MaxDelay time.Duration - - // baseDelay is the amount of time to wait before retrying after the first - // failure. - BaseDelay time.Duration - - // factor is applied to the backoff after each retry. - Factor float64 - - // jitter provides a range to randomize backoff delays. - Jitter float64 -} - -/* -// NOTE TODO avoid use unexcept config. -func (bc *BackoffConfig) Fix() { - md := bc.MaxDelay - *bc = DefaultBackoffConfig - - if md > 0 { - bc.MaxDelay = md - } -} -*/ - -// Backoff returns the amount of time to wait before the next retry given -// the number of consecutive failures. -func (bc *BackoffConfig) Backoff(retries int) time.Duration { - if retries == 0 { - return bc.BaseDelay - } - backoff, max := float64(bc.BaseDelay), float64(bc.MaxDelay) - for backoff < max && retries > 0 { - backoff *= bc.Factor - retries-- - } - if backoff > max { - backoff = max - } - // Randomize backoff delays so that if a cluster of requests start at - // the same time, they won't operate in lockstep. - backoff *= 1 + bc.Jitter*(rand.Float64()*2-1) - if backoff < 0 { - return 0 - } - return time.Duration(backoff) -} diff --git a/pkg/net/netutil/breaker/README.md b/pkg/net/netutil/breaker/README.md deleted file mode 100644 index d4294a9e2..000000000 --- a/pkg/net/netutil/breaker/README.md +++ /dev/null @@ -1,20 +0,0 @@ -#### breaker - -##### 项目简介 -1. 提供熔断器功能,供各种client(如rpc、http、msyql)等进行熔断 -2. 提供Go方法供业务在breaker熔断前后进行回调处理 - -##### 配置说明 -> 1. NewGroup(name string,c *Config)当c==nil时则采用默认配置 -> 2. 可通过breaker.Init(c *Config)替换默认配置 -> 3. 可通过group.Reload(c *Config)进行配置更新 -> 4. 默认配置如下所示: - _conf = &Config{ - Window: xtime.Duration(3 * time.Second), - Bucket: 10, - Request: 100, - K:1.5, - } - -##### 测试 -1. 执行当前目录下所有测试文件,测试所有功能 diff --git a/pkg/net/netutil/breaker/breaker.go b/pkg/net/netutil/breaker/breaker.go deleted file mode 100644 index c8aa87564..000000000 --- a/pkg/net/netutil/breaker/breaker.go +++ /dev/null @@ -1,164 +0,0 @@ -package breaker - -import ( - "sync" - "time" - - xtime "github.com/go-kratos/kratos/pkg/time" -) - -// Config broker config. -type Config struct { - SwitchOff bool // breaker switch,default off. - - // Google - K float64 - - Window xtime.Duration - Bucket int - Request int64 -} - -func (conf *Config) fix() { - if conf.K == 0 { - conf.K = 1.5 - } - if conf.Request == 0 { - conf.Request = 100 - } - if conf.Bucket == 0 { - conf.Bucket = 10 - } - if conf.Window == 0 { - conf.Window = xtime.Duration(3 * time.Second) - } -} - -// Breaker is a CircuitBreaker pattern. -// FIXME on int32 atomic.LoadInt32(&b.on) == _switchOn -type Breaker interface { - Allow() error - MarkSuccess() - MarkFailed() -} - -// Group represents a class of CircuitBreaker and forms a namespace in which -// units of CircuitBreaker. -type Group struct { - mu sync.RWMutex - brks map[string]Breaker - conf *Config -} - -const ( - // StateOpen when circuit breaker open, request not allowed, after sleep - // some duration, allow one single request for testing the health, if ok - // then state reset to closed, if not continue the step. - StateOpen int32 = iota - // StateClosed when circuit breaker closed, request allowed, the breaker - // calc the succeed ratio, if request num greater request setting and - // ratio lower than the setting ratio, then reset state to open. - StateClosed - // StateHalfopen when circuit breaker open, after slepp some duration, allow - // one request, but not state closed. - StateHalfopen - - //_switchOn int32 = iota - // _switchOff -) - -var ( - _mu sync.RWMutex - _conf = &Config{ - Window: xtime.Duration(3 * time.Second), - Bucket: 10, - Request: 100, - - // Percentage of failures must be lower than 33.33% - K: 1.5, - - // Pattern: "", - } - _group = NewGroup(_conf) -) - -// Init init global breaker config, also can reload config after first time call. -func Init(conf *Config) { - if conf == nil { - return - } - _mu.Lock() - _conf = conf - _mu.Unlock() -} - -// Go runs your function while tracking the breaker state of default group. -func Go(name string, run, fallback func() error) error { - breaker := _group.Get(name) - if err := breaker.Allow(); err != nil { - return fallback() - } - return run() -} - -// newBreaker new a breaker. -func newBreaker(c *Config) (b Breaker) { - // factory - return newSRE(c) -} - -// NewGroup new a breaker group container, if conf nil use default conf. -func NewGroup(conf *Config) *Group { - if conf == nil { - _mu.RLock() - conf = _conf - _mu.RUnlock() - } else { - conf.fix() - } - return &Group{ - conf: conf, - brks: make(map[string]Breaker), - } -} - -// Get get a breaker by a specified key, if breaker not exists then make a new one. -func (g *Group) Get(key string) Breaker { - g.mu.RLock() - brk, ok := g.brks[key] - conf := g.conf - g.mu.RUnlock() - if ok { - return brk - } - // NOTE here may new multi breaker for rarely case, let gc drop it. - brk = newBreaker(conf) - g.mu.Lock() - if _, ok = g.brks[key]; !ok { - g.brks[key] = brk - } - g.mu.Unlock() - return brk -} - -// Reload reload the group by specified config, this may let all inner breaker -// reset to a new one. -func (g *Group) Reload(conf *Config) { - if conf == nil { - return - } - conf.fix() - g.mu.Lock() - g.conf = conf - g.brks = make(map[string]Breaker, len(g.brks)) - g.mu.Unlock() -} - -// Go runs your function while tracking the breaker state of group. -func (g *Group) Go(name string, run, fallback func() error) error { - breaker := g.Get(name) - if err := breaker.Allow(); err != nil { - return fallback() - } - return run() -} diff --git a/pkg/net/netutil/breaker/breaker_test.go b/pkg/net/netutil/breaker/breaker_test.go deleted file mode 100644 index 28bc2870d..000000000 --- a/pkg/net/netutil/breaker/breaker_test.go +++ /dev/null @@ -1,94 +0,0 @@ -package breaker - -import ( - "errors" - "testing" - "time" - - xtime "github.com/go-kratos/kratos/pkg/time" -) - -func TestGroup(t *testing.T) { - g1 := NewGroup(nil) - g2 := NewGroup(_conf) - if g1.conf != g2.conf { - t.FailNow() - } - - brk := g2.Get("key") - brk1 := g2.Get("key1") - if brk == brk1 { - t.FailNow() - } - brk2 := g2.Get("key") - if brk != brk2 { - t.FailNow() - } - - g := NewGroup(_conf) - c := &Config{ - Window: xtime.Duration(1 * time.Second), - Bucket: 10, - Request: 100, - SwitchOff: !_conf.SwitchOff, - } - g.Reload(c) - if g.conf.SwitchOff == _conf.SwitchOff { - t.FailNow() - } -} - -func TestInit(t *testing.T) { - switchOff := _conf.SwitchOff - c := &Config{ - Window: xtime.Duration(3 * time.Second), - Bucket: 10, - Request: 100, - SwitchOff: !switchOff, - } - Init(c) - if _conf.SwitchOff == switchOff { - t.FailNow() - } -} - -func TestGo(t *testing.T) { - if err := Go("test_run", func() error { - t.Log("breaker allow,callback run()") - return nil - }, func() error { - t.Log("breaker not allow,callback fallback()") - return errors.New("breaker not allow") - }); err != nil { - t.Error(err) - } - - _group.Reload(&Config{ - Window: xtime.Duration(3 * time.Second), - Bucket: 10, - Request: 100, - SwitchOff: true, - }) - - if err := Go("test_fallback", func() error { - t.Log("breaker allow,callback run()") - return nil - }, func() error { - t.Log("breaker not allow,callback fallback()") - return nil - }); err != nil { - t.Error(err) - } -} - -func markSuccess(b Breaker, count int) { - for i := 0; i < count; i++ { - b.MarkSuccess() - } -} - -func markFailed(b Breaker, count int) { - for i := 0; i < count; i++ { - b.MarkFailed() - } -} diff --git a/pkg/net/netutil/breaker/example_test.go b/pkg/net/netutil/breaker/example_test.go deleted file mode 100644 index d02943d07..000000000 --- a/pkg/net/netutil/breaker/example_test.go +++ /dev/null @@ -1,60 +0,0 @@ -package breaker_test - -import ( - "fmt" - "time" - - "github.com/go-kratos/kratos/pkg/net/netutil/breaker" - xtime "github.com/go-kratos/kratos/pkg/time" -) - -// ExampleGroup show group usage. -func ExampleGroup() { - c := &breaker.Config{ - Window: xtime.Duration(3 * time.Second), - K: 1.5, - Bucket: 10, - Request: 100, - } - // init default config - breaker.Init(c) - // new group - g := breaker.NewGroup(c) - // reload group config - c.Bucket = 100 - c.Request = 200 - g.Reload(c) - // get breaker by key - g.Get("key") -} - -// ExampleBreaker show breaker usage. -func ExampleBreaker() { - // new group,use default breaker config - g := breaker.NewGroup(nil) - brk := g.Get("key") - // mark request success - brk.MarkSuccess() - // mark request failed - brk.MarkFailed() - // check if breaker allow or not - if brk.Allow() == nil { - fmt.Println("breaker allow") - } else { - fmt.Println("breaker not allow") - } -} - -// ExampleGo this example create a default group and show function callback -// according to the state of breaker. -func ExampleGo() { - run := func() error { - return nil - } - fallback := func() error { - return fmt.Errorf("unknown error") - } - if err := breaker.Go("example_go", run, fallback); err != nil { - fmt.Println(err) - } -} diff --git a/pkg/net/netutil/breaker/sre_breaker.go b/pkg/net/netutil/breaker/sre_breaker.go deleted file mode 100644 index 0caa057db..000000000 --- a/pkg/net/netutil/breaker/sre_breaker.go +++ /dev/null @@ -1,100 +0,0 @@ -package breaker - -import ( - "math" - "math/rand" - "sync" - "sync/atomic" - "time" - - "github.com/go-kratos/kratos/pkg/ecode" - "github.com/go-kratos/kratos/pkg/log" - "github.com/go-kratos/kratos/pkg/stat/metric" -) - -// sreBreaker is a sre CircuitBreaker pattern. -type sreBreaker struct { - stat metric.RollingCounter - r *rand.Rand - // rand.New(...) returns a non thread safe object - randLock sync.Mutex - - k float64 - request int64 - - state int32 -} - -func newSRE(c *Config) Breaker { - counterOpts := metric.RollingCounterOpts{ - Size: c.Bucket, - BucketDuration: time.Duration(int64(c.Window) / int64(c.Bucket)), - } - stat := metric.NewRollingCounter(counterOpts) - return &sreBreaker{ - stat: stat, - r: rand.New(rand.NewSource(time.Now().UnixNano())), - - request: c.Request, - k: c.K, - state: StateClosed, - } -} - -func (b *sreBreaker) summary() (success int64, total int64) { - b.stat.Reduce(func(iterator metric.Iterator) float64 { - for iterator.Next() { - bucket := iterator.Bucket() - total += bucket.Count - for _, p := range bucket.Points { - success += int64(p) - } - } - return 0 - }) - return -} - -func (b *sreBreaker) Allow() error { - success, total := b.summary() - k := b.k * float64(success) - if log.V(5) { - log.Info("breaker: request: %d, succee: %d, fail: %d", total, success, total-success) - } - // check overflow requests = K * success - if total < b.request || float64(total) < k { - if atomic.LoadInt32(&b.state) == StateOpen { - atomic.CompareAndSwapInt32(&b.state, StateOpen, StateClosed) - } - return nil - } - if atomic.LoadInt32(&b.state) == StateClosed { - atomic.CompareAndSwapInt32(&b.state, StateClosed, StateOpen) - } - dr := math.Max(0, (float64(total)-k)/float64(total+1)) - drop := b.trueOnProba(dr) - if log.V(5) { - log.Info("breaker: drop ratio: %f, drop: %t", dr, drop) - } - if drop { - return ecode.ServiceUnavailable - } - return nil -} - -func (b *sreBreaker) MarkSuccess() { - b.stat.Add(1) -} - -func (b *sreBreaker) MarkFailed() { - // NOTE: when client reject requets locally, continue add counter let the - // drop ratio higher. - b.stat.Add(0) -} - -func (b *sreBreaker) trueOnProba(proba float64) (truth bool) { - b.randLock.Lock() - truth = b.r.Float64() < proba - b.randLock.Unlock() - return -} diff --git a/pkg/net/netutil/breaker/sre_breaker_test.go b/pkg/net/netutil/breaker/sre_breaker_test.go deleted file mode 100644 index 1f54cfd4b..000000000 --- a/pkg/net/netutil/breaker/sre_breaker_test.go +++ /dev/null @@ -1,177 +0,0 @@ -package breaker - -import ( - "math" - "math/rand" - "testing" - "time" - - "github.com/go-kratos/kratos/pkg/stat/metric" - xtime "github.com/go-kratos/kratos/pkg/time" - - "github.com/stretchr/testify/assert" -) - -func getSRE() Breaker { - return NewGroup(&Config{ - Window: xtime.Duration(1 * time.Second), - Bucket: 10, - Request: 100, - K: 2, - }).Get("") -} - -func getSREBreaker() *sreBreaker { - counterOpts := metric.RollingCounterOpts{ - Size: 10, - BucketDuration: time.Millisecond * 100, - } - stat := metric.NewRollingCounter(counterOpts) - return &sreBreaker{ - stat: stat, - r: rand.New(rand.NewSource(time.Now().UnixNano())), - - request: 100, - k: 2, - state: StateClosed, - } -} - -func markSuccessWithDuration(b Breaker, count int, sleep time.Duration) { - for i := 0; i < count; i++ { - b.MarkSuccess() - time.Sleep(sleep) - } -} - -func markFailedWithDuration(b Breaker, count int, sleep time.Duration) { - for i := 0; i < count; i++ { - b.MarkFailed() - time.Sleep(sleep) - } -} - -func testSREClose(t *testing.T, b Breaker) { - markSuccess(b, 80) - assert.Equal(t, b.Allow(), nil) - markSuccess(b, 120) - assert.Equal(t, b.Allow(), nil) -} - -func testSREOpen(t *testing.T, b Breaker) { - markSuccess(b, 100) - assert.Equal(t, b.Allow(), nil) - markFailed(b, 10000000) - assert.NotEqual(t, b.Allow(), nil) -} - -func testSREHalfOpen(t *testing.T, b Breaker) { - // failback - assert.Equal(t, b.Allow(), nil) - t.Run("allow single failed", func(t *testing.T) { - markFailed(b, 10000000) - assert.NotEqual(t, b.Allow(), nil) - }) - time.Sleep(2 * time.Second) - t.Run("allow single succeed", func(t *testing.T) { - assert.Equal(t, b.Allow(), nil) - markSuccess(b, 10000000) - assert.Equal(t, b.Allow(), nil) - }) -} - -func TestSRE(t *testing.T) { - b := getSRE() - testSREClose(t, b) - - b = getSRE() - testSREOpen(t, b) - - b = getSRE() - testSREHalfOpen(t, b) -} - -func TestSRESelfProtection(t *testing.T) { - t.Run("total request < 100", func(t *testing.T) { - b := getSRE() - markFailed(b, 99) - assert.Equal(t, b.Allow(), nil) - }) - t.Run("total request > 100, total < 2 * success", func(t *testing.T) { - b := getSRE() - size := rand.Intn(10000000) - succ := int(math.Ceil(float64(size))) + 1 - markSuccess(b, succ) - markFailed(b, size-succ) - assert.Equal(t, b.Allow(), nil) - }) -} - -func TestSRESummary(t *testing.T) { - var ( - b *sreBreaker - succ, total int64 - ) - - sleep := 50 * time.Millisecond - t.Run("succ == total", func(t *testing.T) { - b = getSREBreaker() - markSuccessWithDuration(b, 10, sleep) - succ, total = b.summary() - assert.Equal(t, succ, int64(10)) - assert.Equal(t, total, int64(10)) - }) - - t.Run("fail == total", func(t *testing.T) { - b = getSREBreaker() - markFailedWithDuration(b, 10, sleep) - succ, total = b.summary() - assert.Equal(t, succ, int64(0)) - assert.Equal(t, total, int64(10)) - }) - - t.Run("succ = 1/2 * total, fail = 1/2 * total", func(t *testing.T) { - b = getSREBreaker() - markFailedWithDuration(b, 5, sleep) - markSuccessWithDuration(b, 5, sleep) - succ, total = b.summary() - assert.Equal(t, succ, int64(5)) - assert.Equal(t, total, int64(10)) - }) - - t.Run("auto reset rolling counter", func(t *testing.T) { - time.Sleep(time.Second) - succ, total = b.summary() - assert.Equal(t, succ, int64(0)) - assert.Equal(t, total, int64(0)) - }) -} - -func TestTrueOnProba(t *testing.T) { - const proba = math.Pi / 10 - const total = 100000 - const epsilon = 0.05 - var count int - b := getSREBreaker() - for i := 0; i < total; i++ { - if b.trueOnProba(proba) { - count++ - } - } - - ratio := float64(count) / float64(total) - assert.InEpsilon(t, proba, ratio, epsilon) -} - -func BenchmarkSreBreakerAllow(b *testing.B) { - breaker := getSRE() - b.ResetTimer() - for i := 0; i <= b.N; i++ { - breaker.Allow() - if i%2 == 0 { - breaker.MarkSuccess() - } else { - breaker.MarkFailed() - } - } -} diff --git a/pkg/net/rpc/warden/README.md b/pkg/net/rpc/warden/README.md deleted file mode 100644 index 127502949..000000000 --- a/pkg/net/rpc/warden/README.md +++ /dev/null @@ -1,13 +0,0 @@ -#### net/rpc/warden - -##### 项目简介 - -来自 bilibili 主站技术部的 RPC 框架,融合主站技术部的核心科技,带来如飞一般的体验。 - -##### 编译环境 - -- **请只用 Golang v1.9.x 以上版本编译执行** - -##### 依赖包 - -- [grpc](google.golang.org/grpc) diff --git a/pkg/net/rpc/warden/balancer/p2c/README.md b/pkg/net/rpc/warden/balancer/p2c/README.md deleted file mode 100644 index 97b380952..000000000 --- a/pkg/net/rpc/warden/balancer/p2c/README.md +++ /dev/null @@ -1,5 +0,0 @@ -#### warden/balancer/p2c - -##### 项目简介 - -warden 的 Power of Two Choices (P2C)负载均衡模块,主要用于为每个RPC请求返回一个Server节点以供调用 diff --git a/pkg/net/rpc/warden/balancer/p2c/p2c.go b/pkg/net/rpc/warden/balancer/p2c/p2c.go deleted file mode 100644 index e2bba88af..000000000 --- a/pkg/net/rpc/warden/balancer/p2c/p2c.go +++ /dev/null @@ -1,293 +0,0 @@ -package p2c - -import ( - "context" - "math" - "math/rand" - "strconv" - "sync" - "sync/atomic" - "time" - - "github.com/go-kratos/kratos/pkg/conf/env" - - "github.com/go-kratos/kratos/pkg/log" - nmd "github.com/go-kratos/kratos/pkg/net/metadata" - wmd "github.com/go-kratos/kratos/pkg/net/rpc/warden/internal/metadata" - - "google.golang.org/grpc/balancer" - "google.golang.org/grpc/balancer/base" - "google.golang.org/grpc/codes" - "google.golang.org/grpc/resolver" - "google.golang.org/grpc/status" -) - -const ( - // The mean lifetime of `cost`, it reaches its half-life after Tau*ln(2). - tau = int64(time.Millisecond * 600) - // if statistic not collected,we add a big penalty to endpoint - penalty = uint64(1000 * time.Millisecond * 250) - - forceGap = int64(time.Second * 3) -) - -var _ base.PickerBuilder = &p2cPickerBuilder{} -var _ balancer.Picker = &p2cPicker{} - -// Name is the name of pick of two random choices balancer. -const Name = "p2c" - -// newBuilder creates a new weighted-roundrobin balancer builder. -func newBuilder() balancer.Builder { - return base.NewBalancerBuilder(Name, &p2cPickerBuilder{}) -} - -func init() { - balancer.Register(newBuilder()) -} - -type subConn struct { - // metadata - conn balancer.SubConn - addr resolver.Address - meta wmd.MD - - //client statistic data - lag uint64 - success uint64 - inflight int64 - // server statistic data - svrCPU uint64 - - //last collected timestamp - stamp int64 - //last pick timestamp - pick int64 - // request number in a period time - reqs int64 -} - -func (sc *subConn) valid() bool { - return sc.health() > 500 && atomic.LoadUint64(&sc.svrCPU) < 900 -} - -func (sc *subConn) health() uint64 { - return atomic.LoadUint64(&sc.success) -} - -func (sc *subConn) load() uint64 { - lag := uint64(math.Sqrt(float64(atomic.LoadUint64(&sc.lag))) + 1) - load := atomic.LoadUint64(&sc.svrCPU) * lag * uint64(atomic.LoadInt64(&sc.inflight)) - if load == 0 { - // penalty是初始化没有数据时的惩罚值,默认为1e9 * 250 - load = penalty - } - return load -} - -func (sc *subConn) cost() uint64 { - load := atomic.LoadUint64(&sc.svrCPU) * atomic.LoadUint64(&sc.lag) * uint64(atomic.LoadInt64(&sc.inflight)) - if load == 0 { - // penalty是初始化没有数据时的惩罚值,默认为1e9 * 250 - load = penalty - } - return load -} - -// statistics is info for log -type statistic struct { - addr string - score float64 - cs uint64 - lantency uint64 - cpu uint64 - inflight int64 - reqs int64 -} - -type p2cPickerBuilder struct{} - -func (*p2cPickerBuilder) Build(readySCs map[resolver.Address]balancer.SubConn) balancer.Picker { - p := &p2cPicker{ - colors: make(map[string]*p2cPicker), - r: rand.New(rand.NewSource(time.Now().UnixNano())), - } - for addr, sc := range readySCs { - meta, ok := addr.Metadata.(wmd.MD) - if !ok { - meta = wmd.MD{ - Weight: 10, - } - } - subc := &subConn{ - conn: sc, - addr: addr, - meta: meta, - - svrCPU: 500, - lag: 0, - success: 1000, - inflight: 1, - } - if meta.Color == "" { - p.subConns = append(p.subConns, subc) - continue - } - // if color not empty, use color picker - cp, ok := p.colors[meta.Color] - if !ok { - cp = &p2cPicker{r: rand.New(rand.NewSource(time.Now().UnixNano()))} - p.colors[meta.Color] = cp - } - cp.subConns = append(cp.subConns, subc) - } - return p -} - -type p2cPicker struct { - // subConns is the snapshot of the weighted-roundrobin balancer when this picker was - // created. The slice is immutable. Each Get() will do a round robin - // selection from it and return the selected SubConn. - subConns []*subConn - colors map[string]*p2cPicker - logTs int64 - r *rand.Rand - lk sync.Mutex -} - -func (p *p2cPicker) Pick(ctx context.Context, opts balancer.PickInfo) (balancer.SubConn, func(balancer.DoneInfo), error) { - // FIXME refactor to unify the color logic - color := nmd.String(ctx, nmd.Color) - if color == "" && env.Color != "" { - color = env.Color - } - if color != "" { - if cp, ok := p.colors[color]; ok { - return cp.pick(ctx, opts) - } - } - return p.pick(ctx, opts) -} - -// choose two distinct nodes -func (p *p2cPicker) prePick() (nodeA *subConn, nodeB *subConn) { - for i := 0; i < 3; i++ { - p.lk.Lock() - a := p.r.Intn(len(p.subConns)) - b := p.r.Intn(len(p.subConns) - 1) - p.lk.Unlock() - if b >= a { - b = b + 1 - } - nodeA, nodeB = p.subConns[a], p.subConns[b] - if nodeA.valid() || nodeB.valid() { - break - } - } - return -} - -func (p *p2cPicker) pick(ctx context.Context, opts balancer.PickInfo) (balancer.SubConn, func(balancer.DoneInfo), error) { - var pc, upc *subConn - start := time.Now().UnixNano() - - if len(p.subConns) <= 0 { - return nil, nil, balancer.ErrNoSubConnAvailable - } else if len(p.subConns) == 1 { - pc = p.subConns[0] - } else { - nodeA, nodeB := p.prePick() - // meta.Weight为服务发布者在disocvery中设置的权重 - if nodeA.load()*nodeB.health()*nodeB.meta.Weight > nodeB.load()*nodeA.health()*nodeA.meta.Weight { - pc, upc = nodeB, nodeA - } else { - pc, upc = nodeA, nodeB - } - // 如果选中的节点,在forceGap期间内没有被选中一次,那么强制一次 - // 利用强制的机会,来触发成功率、延迟的衰减 - // 原子锁conn.pick保证并发安全,放行一次 - pick := atomic.LoadInt64(&upc.pick) - if start-pick > forceGap && atomic.CompareAndSwapInt64(&upc.pick, pick, start) { - pc = upc - } - } - - // 节点未发生切换才更新pick时间 - if pc != upc { - atomic.StoreInt64(&pc.pick, start) - } - atomic.AddInt64(&pc.inflight, 1) - atomic.AddInt64(&pc.reqs, 1) - return pc.conn, func(di balancer.DoneInfo) { - atomic.AddInt64(&pc.inflight, -1) - now := time.Now().UnixNano() - // get moving average ratio w - stamp := atomic.SwapInt64(&pc.stamp, now) - td := now - stamp - if td < 0 { - td = 0 - } - w := math.Exp(float64(-td) / float64(tau)) - - lag := now - start - if lag < 0 { - lag = 0 - } - oldLag := atomic.LoadUint64(&pc.lag) - if oldLag == 0 { - w = 0.0 - } - lag = int64(float64(oldLag)*w + float64(lag)*(1.0-w)) - atomic.StoreUint64(&pc.lag, uint64(lag)) - - success := uint64(1000) // error value ,if error set 1 - if di.Err != nil { - if st, ok := status.FromError(di.Err); ok { - // only counter the local grpc error, ignore any business error - if st.Code() != codes.Unknown && st.Code() != codes.OK { - success = 0 - } - } - } - oldSuc := atomic.LoadUint64(&pc.success) - success = uint64(float64(oldSuc)*w + float64(success)*(1.0-w)) - atomic.StoreUint64(&pc.success, success) - - trailer := di.Trailer - if strs, ok := trailer[wmd.CPUUsage]; ok { - if cpu, err2 := strconv.ParseUint(strs[0], 10, 64); err2 == nil && cpu > 0 { - atomic.StoreUint64(&pc.svrCPU, cpu) - } - } - - logTs := atomic.LoadInt64(&p.logTs) - if now-logTs > int64(time.Second*3) { - if atomic.CompareAndSwapInt64(&p.logTs, logTs, now) { - p.printStats() - } - } - }, nil -} - -func (p *p2cPicker) printStats() { - if len(p.subConns) <= 0 { - return - } - stats := make([]statistic, 0, len(p.subConns)) - for _, conn := range p.subConns { - var stat statistic - stat.addr = conn.addr.Addr - stat.cpu = atomic.LoadUint64(&conn.svrCPU) - stat.cs = atomic.LoadUint64(&conn.success) - stat.inflight = atomic.LoadInt64(&conn.inflight) - stat.lantency = atomic.LoadUint64(&conn.lag) - stat.reqs = atomic.SwapInt64(&conn.reqs, 0) - load := conn.load() - if load != 0 { - stat.score = float64(stat.cs*conn.meta.Weight*1e8) / float64(load) - } - stats = append(stats, stat) - } - log.Info("p2c %s : %+v", p.subConns[0].addr.ServerName, stats) - //fmt.Printf("%+v\n", stats) -} diff --git a/pkg/net/rpc/warden/balancer/p2c/p2c_test.go b/pkg/net/rpc/warden/balancer/p2c/p2c_test.go deleted file mode 100644 index c34d87fbd..000000000 --- a/pkg/net/rpc/warden/balancer/p2c/p2c_test.go +++ /dev/null @@ -1,345 +0,0 @@ -package p2c - -import ( - "context" - "flag" - "fmt" - "math/rand" - "strconv" - "sync/atomic" - "testing" - "time" - - "github.com/go-kratos/kratos/pkg/conf/env" - - nmd "github.com/go-kratos/kratos/pkg/net/metadata" - wmeta "github.com/go-kratos/kratos/pkg/net/rpc/warden/internal/metadata" - - "google.golang.org/grpc/balancer" - "google.golang.org/grpc/codes" - "google.golang.org/grpc/metadata" - "google.golang.org/grpc/resolver" - "google.golang.org/grpc/status" -) - -var serverNum int -var cliNum int -var concurrency int -var extraLoad int64 -var extraDelay int64 -var extraWeight uint64 - -func init() { - flag.IntVar(&serverNum, "snum", 6, "-snum 6") - flag.IntVar(&cliNum, "cnum", 12, "-cnum 12") - flag.IntVar(&concurrency, "concurrency", 10, "-cc 10") - flag.Int64Var(&extraLoad, "exload", 3, "-exload 3") - flag.Int64Var(&extraDelay, "exdelay", 250, "-exdelay 250") - flag.Uint64Var(&extraWeight, "extraWeight", 50, "-exdelay 50") -} - -type testSubConn struct { - addr resolver.Address - wait chan struct{} - //statics - reqs int64 - usage int64 - cpu int64 - prevReq int64 - prevUsage int64 - //control params - loadJitter int64 - delayJitter int64 -} - -func newTestSubConn(addr string, weight uint64, color string) (sc *testSubConn) { - sc = &testSubConn{ - addr: resolver.Address{ - Addr: addr, - Metadata: wmeta.MD{ - Weight: weight, - Color: color, - }, - }, - wait: make(chan struct{}, 1000), - } - go func() { - for { - for i := 0; i < 210; i++ { - <-sc.wait - } - time.Sleep(time.Millisecond * 20) - } - }() - - return -} - -func (s *testSubConn) connect(ctx context.Context) { - time.Sleep(time.Millisecond * 15) - //add qps counter when request come in - atomic.AddInt64(&s.reqs, 1) - select { - case <-ctx.Done(): - return - case s.wait <- struct{}{}: - atomic.AddInt64(&s.usage, 1) - } - load := atomic.LoadInt64(&s.loadJitter) - if load > 0 { - for i := 0; i <= rand.Intn(int(load)); i++ { - select { - case <-ctx.Done(): - return - case s.wait <- struct{}{}: - atomic.AddInt64(&s.usage, 1) - } - } - } - delay := atomic.LoadInt64(&s.delayJitter) - if delay > 0 { - delay = rand.Int63n(delay) - time.Sleep(time.Millisecond * time.Duration(delay)) - } -} - -func (s *testSubConn) UpdateAddresses([]resolver.Address) { - -} - -// Connect starts the connecting for this SubConn. -func (s *testSubConn) Connect() { - -} - -func TestBalancerPick(t *testing.T) { - scs := map[resolver.Address]balancer.SubConn{} - sc1 := &testSubConn{ - addr: resolver.Address{ - Addr: "test1", - Metadata: wmeta.MD{ - Weight: 8, - }, - }, - } - sc2 := &testSubConn{ - addr: resolver.Address{ - Addr: "test2", - Metadata: wmeta.MD{ - Weight: 4, - Color: "red", - }, - }, - } - sc3 := &testSubConn{ - addr: resolver.Address{ - Addr: "test3", - Metadata: wmeta.MD{ - Weight: 2, - Color: "red", - }, - }, - } - sc4 := &testSubConn{ - addr: resolver.Address{ - Addr: "test4", - Metadata: wmeta.MD{ - Weight: 2, - Color: "purple", - }, - }, - } - scs[sc1.addr] = sc1 - scs[sc2.addr] = sc2 - scs[sc3.addr] = sc3 - scs[sc4.addr] = sc4 - b := &p2cPickerBuilder{} - picker := b.Build(scs) - res := []string{"test1", "test1", "test1", "test1"} - for i := 0; i < 3; i++ { - conn, _, err := picker.Pick(context.Background(), balancer.PickInfo{}) - if err != nil { - t.Fatalf("picker.Pick failed!idx:=%d", i) - } - sc := conn.(*testSubConn) - if sc.addr.Addr != res[i] { - t.Fatalf("the subconn picked(%s),but expected(%s)", sc.addr.Addr, res[i]) - } - } - - ctx := nmd.NewContext(context.Background(), nmd.New(map[string]interface{}{"color": "black"})) - for i := 0; i < 4; i++ { - conn, _, err := picker.Pick(ctx, balancer.PickInfo{}) - if err != nil { - t.Fatalf("picker.Pick failed!idx:=%d", i) - } - sc := conn.(*testSubConn) - if sc.addr.Addr != res[i] { - t.Fatalf("the (%d) subconn picked(%s),but expected(%s)", i, sc.addr.Addr, res[i]) - } - } - - env.Color = "purple" - ctx2 := context.Background() - for i := 0; i < 4; i++ { - conn, _, err := picker.Pick(ctx2, balancer.PickInfo{}) - if err != nil { - t.Fatalf("picker.Pick failed!idx:=%d", i) - } - sc := conn.(*testSubConn) - if sc.addr.Addr != "test4" { - t.Fatalf("the (%d) subconn picked(%s),but expected(%s)", i, sc.addr.Addr, res[i]) - } - } -} - -func Benchmark_Wrr(b *testing.B) { - scs := map[resolver.Address]balancer.SubConn{} - for i := 0; i < 50; i++ { - addr := resolver.Address{ - Addr: fmt.Sprintf("addr_%d", i), - Metadata: wmeta.MD{Weight: 10}, - } - scs[addr] = &testSubConn{addr: addr} - } - wpb := &p2cPickerBuilder{} - picker := wpb.Build(scs) - opt := balancer.PickInfo{} - ctx := context.Background() - for idx := 0; idx < b.N; idx++ { - _, done, err := picker.Pick(ctx, opt) - if err != nil { - done(balancer.DoneInfo{}) - } - } -} - -func TestChaosPick(t *testing.T) { - flag.Parse() - t.Logf("start chaos test!svrNum:%d cliNum:%d concurrency:%d exLoad:%d exDelay:%d\n", serverNum, cliNum, concurrency, extraLoad, extraDelay) - c := newController(serverNum, cliNum) - c.launch(concurrency) - go c.updateStatics() - go c.control(extraLoad, extraDelay) - time.Sleep(time.Second * 50) -} - -func newController(svrNum int, cliNum int) *controller { - //new servers - servers := []*testSubConn{} - var weight uint64 = 10 - if extraWeight > 0 { - weight = extraWeight - } - for i := 0; i < svrNum; i++ { - weight += extraWeight - sc := newTestSubConn(fmt.Sprintf("addr_%d", i), weight, "") - servers = append(servers, sc) - } - //new clients - var clients []balancer.Picker - scs := map[resolver.Address]balancer.SubConn{} - for _, v := range servers { - scs[v.addr] = v - } - for i := 0; i < cliNum; i++ { - wpb := &p2cPickerBuilder{} - picker := wpb.Build(scs) - clients = append(clients, picker) - } - - c := &controller{ - servers: servers, - clients: clients, - } - return c -} - -type controller struct { - servers []*testSubConn - clients []balancer.Picker -} - -func (c *controller) launch(concurrency int) { - opt := balancer.PickInfo{} - bkg := context.Background() - for i := range c.clients { - for j := 0; j < concurrency; j++ { - picker := c.clients[i] - go func() { - for { - ctx, cancel := context.WithTimeout(bkg, time.Millisecond*250) - sc, done, _ := picker.Pick(ctx, opt) - server := sc.(*testSubConn) - server.connect(ctx) - var err error - if ctx.Err() != nil { - err = status.Errorf(codes.DeadlineExceeded, "dead") - } - cancel() - cpu := atomic.LoadInt64(&server.cpu) - md := make(map[string]string) - md[wmeta.CPUUsage] = strconv.FormatInt(cpu, 10) - done(balancer.DoneInfo{Trailer: metadata.New(md), Err: err}) - time.Sleep(time.Millisecond * 10) - } - }() - } - } -} - -func (c *controller) updateStatics() { - for { - time.Sleep(time.Millisecond * 500) - for _, sc := range c.servers { - usage := atomic.LoadInt64(&sc.usage) - avgCpu := (usage - sc.prevUsage) * 2 - atomic.StoreInt64(&sc.cpu, avgCpu) - sc.prevUsage = usage - } - } -} - -func (c *controller) control(extraLoad, extraDelay int64) { - var chaos int - for { - fmt.Printf("\n") - //make some chaos - n := rand.Intn(3) - chaos = n + 1 - for i := 0; i < chaos; i++ { - if extraLoad > 0 { - degree := rand.Int63n(extraLoad) - degree++ - atomic.StoreInt64(&c.servers[i].loadJitter, degree) - fmt.Printf("set addr_%d load:%d ", i, degree) - } - if extraDelay > 0 { - degree := rand.Int63n(extraDelay) - atomic.StoreInt64(&c.servers[i].delayJitter, degree) - fmt.Printf("set addr_%d delay:%dms ", i, degree) - } - } - fmt.Printf("\n") - sleep := int64(5) - time.Sleep(time.Second * time.Duration(sleep)) - for _, sc := range c.servers { - req := atomic.LoadInt64(&sc.reqs) - qps := (req - sc.prevReq) / sleep - wait := len(sc.wait) - sc.prevReq = req - fmt.Printf("%s qps:%d waits:%d\n", sc.addr.Addr, qps, wait) - } - for _, picker := range c.clients { - p := picker.(*p2cPicker) - p.printStats() - } - fmt.Printf("\n") - //reset chaos - for i := 0; i < chaos; i++ { - atomic.StoreInt64(&c.servers[i].loadJitter, 0) - atomic.StoreInt64(&c.servers[i].delayJitter, 0) - } - chaos = 0 - } -} diff --git a/pkg/net/rpc/warden/balancer/wrr/README.md b/pkg/net/rpc/warden/balancer/wrr/README.md deleted file mode 100644 index 9483e71dd..000000000 --- a/pkg/net/rpc/warden/balancer/wrr/README.md +++ /dev/null @@ -1,5 +0,0 @@ -#### warden/balancer/wrr - -##### 项目简介 - -warden 的 weighted round robin负载均衡模块,主要用于为每个RPC请求返回一个Server节点以供调用 diff --git a/pkg/net/rpc/warden/balancer/wrr/wrr.go b/pkg/net/rpc/warden/balancer/wrr/wrr.go deleted file mode 100644 index 605f74010..000000000 --- a/pkg/net/rpc/warden/balancer/wrr/wrr.go +++ /dev/null @@ -1,302 +0,0 @@ -package wrr - -import ( - "context" - "math" - "strconv" - "sync" - "sync/atomic" - "time" - - "google.golang.org/grpc" - "google.golang.org/grpc/balancer" - "google.golang.org/grpc/balancer/base" - "google.golang.org/grpc/codes" - "google.golang.org/grpc/metadata" - "google.golang.org/grpc/resolver" - "google.golang.org/grpc/status" - - "github.com/go-kratos/kratos/pkg/conf/env" - "github.com/go-kratos/kratos/pkg/log" - nmd "github.com/go-kratos/kratos/pkg/net/metadata" - wmeta "github.com/go-kratos/kratos/pkg/net/rpc/warden/internal/metadata" - "github.com/go-kratos/kratos/pkg/stat/metric" -) - -var _ base.PickerBuilder = &wrrPickerBuilder{} -var _ balancer.Picker = &wrrPicker{} - -// var dwrrFeature feature.Feature = "dwrr" - -// Name is the name of round_robin balancer. -const Name = "wrr" - -// newBuilder creates a new weighted-roundrobin balancer builder. -func newBuilder() balancer.Builder { - return base.NewBalancerBuilder(Name, &wrrPickerBuilder{}) -} - -func init() { - //feature.DefaultGate.Add(map[feature.Feature]feature.Spec{ - // dwrrFeature: {Default: false}, - //}) - - balancer.Register(newBuilder()) -} - -type serverInfo struct { - cpu int64 - success uint64 // float64 bits -} - -type subConn struct { - conn balancer.SubConn - addr resolver.Address - meta wmeta.MD - - err metric.RollingCounter - latency metric.RollingGauge - si serverInfo - // effective weight - ewt int64 - // current weight - cwt int64 - // last score - score float64 -} - -func (c *subConn) errSummary() (err int64, req int64) { - c.err.Reduce(func(iterator metric.Iterator) float64 { - for iterator.Next() { - bucket := iterator.Bucket() - req += bucket.Count - for _, p := range bucket.Points { - err += int64(p) - } - } - return 0 - }) - return -} - -func (c *subConn) latencySummary() (latency float64, count int64) { - c.latency.Reduce(func(iterator metric.Iterator) float64 { - for iterator.Next() { - bucket := iterator.Bucket() - count += bucket.Count - for _, p := range bucket.Points { - latency += p - } - } - return 0 - }) - return latency / float64(count), count -} - -// statistics is info for log -type statistics struct { - addr string - ewt int64 - cs float64 - ss float64 - latency float64 - cpu float64 - req int64 -} - -// Stats is grpc Interceptor for client to collect server stats -func Stats() grpc.UnaryClientInterceptor { - return func(ctx context.Context, method string, req, reply interface{}, cc *grpc.ClientConn, invoker grpc.UnaryInvoker, opts ...grpc.CallOption) (err error) { - var ( - trailer metadata.MD - md nmd.MD - ok bool - ) - if md, ok = nmd.FromContext(ctx); !ok { - md = nmd.MD{} - } else { - md = md.Copy() - } - ctx = nmd.NewContext(ctx, md) - opts = append(opts, grpc.Trailer(&trailer)) - - err = invoker(ctx, method, req, reply, cc, opts...) - - conn, ok := md["conn"].(*subConn) - if !ok { - return - } - if strs, ok := trailer[wmeta.CPUUsage]; ok { - if cpu, err2 := strconv.ParseInt(strs[0], 10, 64); err2 == nil && cpu > 0 { - atomic.StoreInt64(&conn.si.cpu, cpu) - } - } - return - } -} - -type wrrPickerBuilder struct{} - -func (*wrrPickerBuilder) Build(readySCs map[resolver.Address]balancer.SubConn) balancer.Picker { - p := &wrrPicker{ - colors: make(map[string]*wrrPicker), - } - for addr, sc := range readySCs { - meta, ok := addr.Metadata.(wmeta.MD) - if !ok { - meta = wmeta.MD{ - Weight: 10, - } - } - subc := &subConn{ - conn: sc, - addr: addr, - - meta: meta, - ewt: int64(meta.Weight), - score: -1, - - err: metric.NewRollingCounter(metric.RollingCounterOpts{ - Size: 10, - BucketDuration: time.Millisecond * 100, - }), - latency: metric.NewRollingGauge(metric.RollingGaugeOpts{ - Size: 10, - BucketDuration: time.Millisecond * 100, - }), - - si: serverInfo{cpu: 500, success: math.Float64bits(1)}, - } - if meta.Color == "" { - p.subConns = append(p.subConns, subc) - continue - } - // if color not empty, use color picker - cp, ok := p.colors[meta.Color] - if !ok { - cp = &wrrPicker{} - p.colors[meta.Color] = cp - } - cp.subConns = append(cp.subConns, subc) - } - return p -} - -type wrrPicker struct { - // subConns is the snapshot of the weighted-roundrobin balancer when this picker was - // created. The slice is immutable. Each Get() will do a round robin - // selection from it and return the selected SubConn. - subConns []*subConn - colors map[string]*wrrPicker - updateAt int64 - - mu sync.Mutex -} - -func (p *wrrPicker) Pick(ctx context.Context, opts balancer.PickInfo) (balancer.SubConn, func(balancer.DoneInfo), error) { - // FIXME refactor to unify the color logic - color := nmd.String(ctx, nmd.Color) - if color == "" && env.Color != "" { - color = env.Color - } - if color != "" { - if cp, ok := p.colors[color]; ok { - return cp.pick(ctx, opts) - } - } - return p.pick(ctx, opts) -} - -func (p *wrrPicker) pick(ctx context.Context, opts balancer.PickInfo) (balancer.SubConn, func(balancer.DoneInfo), error) { - var ( - conn *subConn - totalWeight int64 - ) - if len(p.subConns) <= 0 { - return nil, nil, balancer.ErrNoSubConnAvailable - } - p.mu.Lock() - // nginx wrr load balancing algorithm: http://blog.csdn.net/zhangskd/article/details/50194069 - for _, sc := range p.subConns { - totalWeight += sc.ewt - sc.cwt += sc.ewt - if conn == nil || conn.cwt < sc.cwt { - conn = sc - } - } - conn.cwt -= totalWeight - p.mu.Unlock() - start := time.Now() - if cmd, ok := nmd.FromContext(ctx); ok { - cmd["conn"] = conn - } - //if !feature.DefaultGate.Enabled(dwrrFeature) { - // return conn.conn, nil, nil - //} - return conn.conn, func(di balancer.DoneInfo) { - ev := int64(0) // error value ,if error set 1 - if di.Err != nil { - if st, ok := status.FromError(di.Err); ok { - // only counter the local grpc error, ignore any business error - if st.Code() != codes.Unknown && st.Code() != codes.OK { - ev = 1 - } - } - } - conn.err.Add(ev) - - now := time.Now() - conn.latency.Add(now.Sub(start).Nanoseconds() / 1e5) - u := atomic.LoadInt64(&p.updateAt) - if now.UnixNano()-u < int64(time.Second) { - return - } - if !atomic.CompareAndSwapInt64(&p.updateAt, u, now.UnixNano()) { - return - } - var ( - stats = make([]statistics, len(p.subConns)) - count int - total float64 - ) - for i, conn := range p.subConns { - cpu := float64(atomic.LoadInt64(&conn.si.cpu)) - ss := math.Float64frombits(atomic.LoadUint64(&conn.si.success)) - errc, req := conn.errSummary() - lagv, lagc := conn.latencySummary() - - if req > 0 && lagc > 0 && lagv > 0 { - // client-side success ratio - cs := 1 - (float64(errc) / float64(req)) - if cs <= 0 { - cs = 0.1 - } else if cs <= 0.2 && req <= 5 { - cs = 0.2 - } - conn.score = math.Sqrt((cs * ss * ss * 1e9) / (lagv * cpu)) - stats[i] = statistics{cs: cs, ss: ss, latency: lagv, cpu: cpu, req: req} - } - stats[i].addr = conn.addr.Addr - - if conn.score > 0 { - total += conn.score - count++ - } - } - // count must be greater than 1,otherwise will lead ewt to 0 - if count < 2 { - return - } - avgscore := total / float64(count) - p.mu.Lock() - for i, conn := range p.subConns { - if conn.score <= 0 { - conn.score = avgscore - } - conn.ewt = int64(conn.score * float64(conn.meta.Weight)) - stats[i].ewt = conn.ewt - } - p.mu.Unlock() - log.Info("warden wrr(%s): %+v", conn.addr.ServerName, stats) - }, nil -} diff --git a/pkg/net/rpc/warden/balancer/wrr/wrr_test.go b/pkg/net/rpc/warden/balancer/wrr/wrr_test.go deleted file mode 100644 index e7cc1cda1..000000000 --- a/pkg/net/rpc/warden/balancer/wrr/wrr_test.go +++ /dev/null @@ -1,190 +0,0 @@ -package wrr - -import ( - "context" - "fmt" - "testing" - "time" - - "google.golang.org/grpc/codes" - "google.golang.org/grpc/status" - - "github.com/go-kratos/kratos/pkg/conf/env" - nmd "github.com/go-kratos/kratos/pkg/net/metadata" - wmeta "github.com/go-kratos/kratos/pkg/net/rpc/warden/internal/metadata" - "github.com/go-kratos/kratos/pkg/stat/metric" - - "github.com/stretchr/testify/assert" - "google.golang.org/grpc/balancer" - "google.golang.org/grpc/resolver" -) - -type testSubConn struct { - addr resolver.Address -} - -func (s *testSubConn) UpdateAddresses([]resolver.Address) { - -} - -// Connect starts the connecting for this SubConn. -func (s *testSubConn) Connect() { - fmt.Println(s.addr.Addr) -} - -func TestBalancerPick(t *testing.T) { - scs := map[resolver.Address]balancer.SubConn{} - sc1 := &testSubConn{ - addr: resolver.Address{ - Addr: "test1", - Metadata: wmeta.MD{ - Weight: 8, - }, - }, - } - sc2 := &testSubConn{ - addr: resolver.Address{ - Addr: "test2", - Metadata: wmeta.MD{ - Weight: 4, - Color: "red", - }, - }, - } - sc3 := &testSubConn{ - addr: resolver.Address{ - Addr: "test3", - Metadata: wmeta.MD{ - Weight: 2, - Color: "red", - }, - }, - } - scs[sc1.addr] = sc1 - scs[sc2.addr] = sc2 - scs[sc3.addr] = sc3 - b := &wrrPickerBuilder{} - picker := b.Build(scs) - res := []string{"test1", "test1", "test1", "test1"} - for i := 0; i < 3; i++ { - conn, _, err := picker.Pick(context.Background(), balancer.PickInfo{}) - if err != nil { - t.Fatalf("picker.Pick failed!idx:=%d", i) - } - sc := conn.(*testSubConn) - if sc.addr.Addr != res[i] { - t.Fatalf("the subconn picked(%s),but expected(%s)", sc.addr.Addr, res[i]) - } - } - res2 := []string{"test2", "test3", "test2", "test2", "test3", "test2"} - ctx := nmd.NewContext(context.Background(), nmd.New(map[string]interface{}{"color": "red"})) - for i := 0; i < 6; i++ { - conn, _, err := picker.Pick(ctx, balancer.PickInfo{}) - if err != nil { - t.Fatalf("picker.Pick failed!idx:=%d", i) - } - sc := conn.(*testSubConn) - if sc.addr.Addr != res2[i] { - t.Fatalf("the (%d) subconn picked(%s),but expected(%s)", i, sc.addr.Addr, res2[i]) - } - } - ctx = nmd.NewContext(context.Background(), nmd.New(map[string]interface{}{"color": "black"})) - for i := 0; i < 4; i++ { - conn, _, err := picker.Pick(ctx, balancer.PickInfo{}) - if err != nil { - t.Fatalf("picker.Pick failed!idx:=%d", i) - } - sc := conn.(*testSubConn) - if sc.addr.Addr != res[i] { - t.Fatalf("the (%d) subconn picked(%s),but expected(%s)", i, sc.addr.Addr, res[i]) - } - } - - // test for env color - ctx = context.Background() - env.Color = "red" - for i := 0; i < 6; i++ { - conn, _, err := picker.Pick(ctx, balancer.PickInfo{}) - if err != nil { - t.Fatalf("picker.Pick failed!idx:=%d", i) - } - sc := conn.(*testSubConn) - if sc.addr.Addr != res2[i] { - t.Fatalf("the (%d) subconn picked(%s),but expected(%s)", i, sc.addr.Addr, res2[i]) - } - } -} - -func TestBalancerDone(t *testing.T) { - scs := map[resolver.Address]balancer.SubConn{} - sc1 := &testSubConn{ - addr: resolver.Address{ - Addr: "test1", - Metadata: wmeta.MD{ - Weight: 8, - }, - }, - } - scs[sc1.addr] = sc1 - b := &wrrPickerBuilder{} - picker := b.Build(scs) - - _, done, _ := picker.Pick(context.Background(), balancer.PickInfo{}) - time.Sleep(100 * time.Millisecond) - done(balancer.DoneInfo{Err: status.Errorf(codes.Unknown, "test")}) - err, req := picker.(*wrrPicker).subConns[0].errSummary() - assert.Equal(t, int64(0), err) - assert.Equal(t, int64(1), req) - - latency, count := picker.(*wrrPicker).subConns[0].latencySummary() - expectLatency := float64(100*time.Millisecond) / 1e5 - if latency < expectLatency || latency > (expectLatency+500) { - t.Fatalf("latency is less than 100ms or greater than 150ms, %f", latency) - } - assert.Equal(t, int64(1), count) - - _, done, _ = picker.Pick(context.Background(), balancer.PickInfo{}) - done(balancer.DoneInfo{Err: status.Errorf(codes.Aborted, "test")}) - err, req = picker.(*wrrPicker).subConns[0].errSummary() - assert.Equal(t, int64(1), err) - assert.Equal(t, int64(2), req) -} - -func TestErrSummary(t *testing.T) { - sc := &subConn{ - err: metric.NewRollingCounter(metric.RollingCounterOpts{ - Size: 10, - BucketDuration: time.Millisecond * 100, - }), - latency: metric.NewRollingGauge(metric.RollingGaugeOpts{ - Size: 10, - BucketDuration: time.Millisecond * 100, - }), - } - for i := 0; i < 10; i++ { - sc.err.Add(0) - sc.err.Add(1) - } - err, req := sc.errSummary() - assert.Equal(t, int64(10), err) - assert.Equal(t, int64(20), req) -} - -func TestLatencySummary(t *testing.T) { - sc := &subConn{ - err: metric.NewRollingCounter(metric.RollingCounterOpts{ - Size: 10, - BucketDuration: time.Millisecond * 100, - }), - latency: metric.NewRollingGauge(metric.RollingGaugeOpts{ - Size: 10, - BucketDuration: time.Millisecond * 100, - }), - } - for i := 1; i <= 100; i++ { - sc.latency.Add(int64(i)) - } - latency, count := sc.latencySummary() - assert.Equal(t, 50.50, latency) - assert.Equal(t, int64(100), count) -} diff --git a/pkg/net/rpc/warden/client.go b/pkg/net/rpc/warden/client.go deleted file mode 100644 index 995a907d3..000000000 --- a/pkg/net/rpc/warden/client.go +++ /dev/null @@ -1,381 +0,0 @@ -package warden - -import ( - "context" - "fmt" - "net/url" - "os" - "strconv" - "strings" - "sync" - "time" - - "github.com/go-kratos/kratos/pkg/net/rpc/warden/resolver" - "github.com/go-kratos/kratos/pkg/net/rpc/warden/resolver/direct" - - "github.com/go-kratos/kratos/pkg/conf/env" - "github.com/go-kratos/kratos/pkg/conf/flagvar" - "github.com/go-kratos/kratos/pkg/ecode" - "github.com/go-kratos/kratos/pkg/naming" - nmd "github.com/go-kratos/kratos/pkg/net/metadata" - "github.com/go-kratos/kratos/pkg/net/netutil/breaker" - "github.com/go-kratos/kratos/pkg/net/rpc/warden/balancer/p2c" - "github.com/go-kratos/kratos/pkg/net/rpc/warden/internal/status" - "github.com/go-kratos/kratos/pkg/net/trace" - xtime "github.com/go-kratos/kratos/pkg/time" - - "github.com/pkg/errors" - "google.golang.org/grpc" - "google.golang.org/grpc/credentials" - "google.golang.org/grpc/keepalive" - "google.golang.org/grpc/metadata" - "google.golang.org/grpc/peer" - gstatus "google.golang.org/grpc/status" -) - -var _grpcTarget flagvar.StringVars - -var ( - _once sync.Once - _defaultCliConf = &ClientConfig{ - Dial: xtime.Duration(time.Second * 10), - Timeout: xtime.Duration(time.Millisecond * 250), - Subset: 50, - KeepAliveInterval: xtime.Duration(time.Second * 60), - KeepAliveTimeout: xtime.Duration(time.Second * 20), - } - _defaultClient *Client -) - -func baseMetadata() metadata.MD { - gmd := metadata.MD{nmd.Caller: []string{env.AppID}} - if env.Color != "" { - gmd[nmd.Color] = []string{env.Color} - } - return gmd -} - -// Register direct resolver by default to handle direct:// scheme. -func init() { - resolver.Register(direct.New()) -} - -// ClientConfig is rpc client conf. -type ClientConfig struct { - Dial xtime.Duration - Timeout xtime.Duration - Breaker *breaker.Config - Method map[string]*ClientConfig - Clusters []string - Zone string - Subset int - NonBlock bool - KeepAliveInterval xtime.Duration - KeepAliveTimeout xtime.Duration - KeepAliveWithoutStream bool -} - -// Client is the framework's client side instance, it contains the ctx, opt and interceptors. -// Create an instance of Client, by using NewClient(). -type Client struct { - conf *ClientConfig - breaker *breaker.Group - mutex sync.RWMutex - - opts []grpc.DialOption - handlers []grpc.UnaryClientInterceptor -} - -// TimeoutCallOption timeout option. -type TimeoutCallOption struct { - *grpc.EmptyCallOption - Timeout time.Duration -} - -// WithTimeoutCallOption can override the timeout in ctx and the timeout in the configuration file -func WithTimeoutCallOption(timeout time.Duration) *TimeoutCallOption { - return &TimeoutCallOption{&grpc.EmptyCallOption{}, timeout} -} - -// handle returns a new unary client interceptor for OpenTracing\Logging\LinkTimeout. -func (c *Client) handle() grpc.UnaryClientInterceptor { - return func(ctx context.Context, method string, req, reply interface{}, cc *grpc.ClientConn, invoker grpc.UnaryInvoker, opts ...grpc.CallOption) (err error) { - var ( - ok bool - t trace.Trace - gmd metadata.MD - conf *ClientConfig - cancel context.CancelFunc - addr string - p peer.Peer - ) - var ec ecode.Codes = ecode.OK - // apm tracing - if t, ok = trace.FromContext(ctx); ok { - t = t.Fork("", method) - defer t.Finish(&err) - } - - // setup metadata - gmd = baseMetadata() - trace.Inject(t, trace.GRPCFormat, gmd) - c.mutex.RLock() - if conf, ok = c.conf.Method[method]; !ok { - conf = c.conf - } - c.mutex.RUnlock() - brk := c.breaker.Get(method) - if err = brk.Allow(); err != nil { - _metricClientReqCodeTotal.Inc(method, "breaker") - return - } - defer onBreaker(brk, &err) - var timeOpt *TimeoutCallOption - for _, opt := range opts { - var tok bool - timeOpt, tok = opt.(*TimeoutCallOption) - if tok { - break - } - } - if timeOpt != nil && timeOpt.Timeout > 0 { - ctx, cancel = context.WithTimeout(nmd.WithContext(ctx), timeOpt.Timeout) - } else { - _, ctx, cancel = conf.Timeout.Shrink(ctx) - } - - defer cancel() - nmd.Range(ctx, - func(key string, value interface{}) { - if valstr, ok := value.(string); ok { - gmd[key] = []string{valstr} - } - }, - nmd.IsOutgoingKey) - // merge with old matadata if exists - if oldmd, ok := metadata.FromOutgoingContext(ctx); ok { - gmd = metadata.Join(gmd, oldmd) - } - ctx = metadata.NewOutgoingContext(ctx, gmd) - - opts = append(opts, grpc.Peer(&p)) - if err = invoker(ctx, method, req, reply, cc, opts...); err != nil { - gst, _ := gstatus.FromError(err) - ec = status.ToEcode(gst) - err = errors.WithMessage(ec, gst.Message()) - } - if p.Addr != nil { - addr = p.Addr.String() - } - if t != nil { - t.SetTag(trace.String(trace.TagAddress, addr), trace.String(trace.TagComment, "")) - } - return - } -} - -func onBreaker(breaker breaker.Breaker, err *error) { - if err != nil && *err != nil { - if ecode.EqualError(ecode.ServerErr, *err) || ecode.EqualError(ecode.ServiceUnavailable, *err) || ecode.EqualError(ecode.Deadline, *err) || ecode.EqualError(ecode.LimitExceed, *err) { - breaker.MarkFailed() - return - } - } - breaker.MarkSuccess() -} - -// NewConn will create a grpc conn by default config. -func NewConn(target string, opt ...grpc.DialOption) (*grpc.ClientConn, error) { - return DefaultClient().Dial(context.Background(), target, opt...) -} - -// NewClient returns a new blank Client instance with a default client interceptor. -// opt can be used to add grpc dial options. -func NewClient(conf *ClientConfig, opt ...grpc.DialOption) *Client { - c := new(Client) - if err := c.SetConfig(conf); err != nil { - panic(err) - } - c.UseOpt(grpc.WithBalancerName(p2c.Name)) - c.UseOpt(opt...) - return c -} - -// DefaultClient returns a new default Client instance with a default client interceptor and default dialoption. -// opt can be used to add grpc dial options. -func DefaultClient() *Client { - _once.Do(func() { - _defaultClient = NewClient(nil) - }) - return _defaultClient -} - -// SetConfig hot reloads client config -func (c *Client) SetConfig(conf *ClientConfig) (err error) { - if conf == nil { - conf = _defaultCliConf - } - if conf.Dial <= 0 { - conf.Dial = xtime.Duration(time.Second * 10) - } - if conf.Timeout <= 0 { - conf.Timeout = xtime.Duration(time.Millisecond * 250) - } - if conf.Subset <= 0 { - conf.Subset = 50 - } - if conf.KeepAliveInterval <= 0 { - conf.KeepAliveInterval = xtime.Duration(time.Second * 60) - } - if conf.KeepAliveTimeout <= 0 { - conf.KeepAliveTimeout = xtime.Duration(time.Second * 20) - } - - // FIXME(maojian) check Method dial/timeout - c.mutex.Lock() - c.conf = conf - if c.breaker == nil { - c.breaker = breaker.NewGroup(conf.Breaker) - } else { - c.breaker.Reload(conf.Breaker) - } - c.mutex.Unlock() - return nil -} - -// Use attachs a global inteceptor to the Client. -// For example, this is the right place for a circuit breaker or error management inteceptor. -func (c *Client) Use(handlers ...grpc.UnaryClientInterceptor) *Client { - finalSize := len(c.handlers) + len(handlers) - if finalSize >= int(_abortIndex) { - panic("warden: client use too many handlers") - } - mergedHandlers := make([]grpc.UnaryClientInterceptor, finalSize) - copy(mergedHandlers, c.handlers) - copy(mergedHandlers[len(c.handlers):], handlers) - c.handlers = mergedHandlers - return c -} - -// UseOpt attachs a global grpc DialOption to the Client. -func (c *Client) UseOpt(opts ...grpc.DialOption) *Client { - c.opts = append(c.opts, opts...) - return c -} - -func (c *Client) cloneOpts() []grpc.DialOption { - dialOptions := make([]grpc.DialOption, len(c.opts)) - copy(dialOptions, c.opts) - return dialOptions -} - -func (c *Client) dial(ctx context.Context, target string, opts ...grpc.DialOption) (conn *grpc.ClientConn, err error) { - dialOptions := c.cloneOpts() - if !c.conf.NonBlock { - dialOptions = append(dialOptions, grpc.WithBlock()) - } - dialOptions = append(dialOptions, grpc.WithKeepaliveParams(keepalive.ClientParameters{ - Time: time.Duration(c.conf.KeepAliveInterval), - Timeout: time.Duration(c.conf.KeepAliveTimeout), - PermitWithoutStream: !c.conf.KeepAliveWithoutStream, - })) - dialOptions = append(dialOptions, opts...) - - // init default handler - var handlers []grpc.UnaryClientInterceptor - handlers = append(handlers, c.recovery()) - handlers = append(handlers, clientLogging(dialOptions...)) - handlers = append(handlers, c.handlers...) - // NOTE: c.handle must be a last interceptor. - handlers = append(handlers, c.handle()) - - dialOptions = append(dialOptions, grpc.WithUnaryInterceptor(chainUnaryClient(handlers))) - c.mutex.RLock() - conf := c.conf - c.mutex.RUnlock() - if conf.Dial > 0 { - var cancel context.CancelFunc - ctx, cancel = context.WithTimeout(ctx, time.Duration(conf.Dial)) - defer cancel() - } - if u, e := url.Parse(target); e == nil { - v := u.Query() - for _, c := range c.conf.Clusters { - v.Add(naming.MetaCluster, c) - } - if c.conf.Zone != "" { - v.Add(naming.MetaZone, c.conf.Zone) - } - if v.Get("subset") == "" && c.conf.Subset > 0 { - v.Add("subset", strconv.FormatInt(int64(c.conf.Subset), 10)) - } - u.RawQuery = v.Encode() - // 比较_grpcTarget中的appid是否等于u.path中的appid,并替换成mock的地址 - for _, t := range _grpcTarget { - strs := strings.SplitN(t, "=", 2) - if len(strs) == 2 && ("/"+strs[0]) == u.Path { - u.Path = "/" + strs[1] - u.Scheme = "passthrough" - u.RawQuery = "" - break - } - } - target = u.String() - } - if conn, err = grpc.DialContext(ctx, target, dialOptions...); err != nil { - fmt.Fprintf(os.Stderr, "warden client: dial %s error %v!", target, err) - } - err = errors.WithStack(err) - return -} - -// Dial creates a client connection to the given target. -// Target format is scheme://authority/endpoint?query_arg=value -// example: discovery://default/account.account.service?cluster=shfy01&cluster=shfy02 -func (c *Client) Dial(ctx context.Context, target string, opts ...grpc.DialOption) (conn *grpc.ClientConn, err error) { - opts = append(opts, grpc.WithInsecure()) - return c.dial(ctx, target, opts...) -} - -// DialTLS creates a client connection over tls transport to the given target. -func (c *Client) DialTLS(ctx context.Context, target string, file string, name string, opts ...grpc.DialOption) (conn *grpc.ClientConn, err error) { - var creds credentials.TransportCredentials - creds, err = credentials.NewClientTLSFromFile(file, name) - if err != nil { - err = errors.WithStack(err) - return - } - opts = append(opts, grpc.WithTransportCredentials(creds)) - return c.dial(ctx, target, opts...) -} - -// chainUnaryClient creates a single interceptor out of a chain of many interceptors. -// -// Execution is done in left-to-right order, including passing of context. -// For example ChainUnaryClient(one, two, three) will execute one before two before three. -func chainUnaryClient(handlers []grpc.UnaryClientInterceptor) grpc.UnaryClientInterceptor { - n := len(handlers) - if n == 0 { - return func(ctx context.Context, method string, req, reply interface{}, - cc *grpc.ClientConn, invoker grpc.UnaryInvoker, opts ...grpc.CallOption) error { - return invoker(ctx, method, req, reply, cc, opts...) - } - } - - return func(ctx context.Context, method string, req, reply interface{}, - cc *grpc.ClientConn, invoker grpc.UnaryInvoker, opts ...grpc.CallOption) error { - var ( - i int - chainHandler grpc.UnaryInvoker - ) - chainHandler = func(ictx context.Context, imethod string, ireq, ireply interface{}, ic *grpc.ClientConn, iopts ...grpc.CallOption) error { - if i == n-1 { - return invoker(ictx, imethod, ireq, ireply, ic, iopts...) - } - i++ - return handlers[i](ictx, imethod, ireq, ireply, ic, chainHandler, iopts...) - } - - return handlers[0](ctx, method, req, reply, cc, chainHandler, opts...) - } -} diff --git a/pkg/net/rpc/warden/client_test.go b/pkg/net/rpc/warden/client_test.go deleted file mode 100644 index 96be46368..000000000 --- a/pkg/net/rpc/warden/client_test.go +++ /dev/null @@ -1,34 +0,0 @@ -package warden - -import ( - "context" - "testing" - - "github.com/stretchr/testify/assert" - "google.golang.org/grpc" -) - -func TestChainUnaryClient(t *testing.T) { - var orders []string - factory := func(name string) grpc.UnaryClientInterceptor { - return func(ctx context.Context, method string, req, reply interface{}, cc *grpc.ClientConn, invoker grpc.UnaryInvoker, opts ...grpc.CallOption) error { - orders = append(orders, name+"-in") - err := invoker(ctx, method, req, reply, cc, opts...) - orders = append(orders, name+"-out") - return err - } - } - handlers := []grpc.UnaryClientInterceptor{factory("h1"), factory("h2"), factory("h3")} - interceptor := chainUnaryClient(handlers) - interceptor(context.Background(), "test", nil, nil, nil, func(context.Context, string, interface{}, interface{}, *grpc.ClientConn, ...grpc.CallOption) error { - return nil - }) - assert.Equal(t, []string{ - "h1-in", - "h2-in", - "h3-in", - "h3-out", - "h2-out", - "h1-out", - }, orders) -} diff --git a/pkg/net/rpc/warden/exapmle_test.go b/pkg/net/rpc/warden/exapmle_test.go deleted file mode 100644 index 0425d70dd..000000000 --- a/pkg/net/rpc/warden/exapmle_test.go +++ /dev/null @@ -1,89 +0,0 @@ -package warden_test - -import ( - "context" - "fmt" - "io" - "time" - - "github.com/go-kratos/kratos/pkg/log" - "github.com/go-kratos/kratos/pkg/net/netutil/breaker" - "github.com/go-kratos/kratos/pkg/net/rpc/warden" - pb "github.com/go-kratos/kratos/pkg/net/rpc/warden/internal/proto/testproto" - xtime "github.com/go-kratos/kratos/pkg/time" - - "google.golang.org/grpc" -) - -type helloServer struct { -} - -func (s *helloServer) SayHello(ctx context.Context, in *pb.HelloRequest) (*pb.HelloReply, error) { - return &pb.HelloReply{Message: "Hello " + in.Name, Success: true}, nil -} - -func (s *helloServer) StreamHello(ss pb.Greeter_StreamHelloServer) error { - for i := 0; i < 3; i++ { - in, err := ss.Recv() - if err == io.EOF { - return nil - } - if err != nil { - return err - } - ret := &pb.HelloReply{Message: "Hello " + in.Name, Success: true} - err = ss.Send(ret) - if err != nil { - return err - } - } - return nil -} - -func ExampleServer() { - s := warden.NewServer(&warden.ServerConfig{Timeout: xtime.Duration(time.Second), Addr: ":8080"}) - // apply server interceptor middleware - s.Use(func(ctx context.Context, req interface{}, args *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (interface{}, error) { - newctx, cancel := context.WithTimeout(ctx, time.Second*10) - defer cancel() - resp, err := handler(newctx, req) - return resp, err - }) - pb.RegisterGreeterServer(s.Server(), &helloServer{}) - s.Start() -} - -func ExampleClient() { - client := warden.NewClient(&warden.ClientConfig{ - Dial: xtime.Duration(time.Second * 10), - Timeout: xtime.Duration(time.Second * 10), - Breaker: &breaker.Config{ - Window: xtime.Duration(3 * time.Second), - Bucket: 10, - K: 1.5, - Request: 20, - }, - }) - // apply client interceptor middleware - client.Use(func(ctx context.Context, method string, req, reply interface{}, cc *grpc.ClientConn, invoker grpc.UnaryInvoker, opts ...grpc.CallOption) (ret error) { - newctx, cancel := context.WithTimeout(ctx, time.Second*5) - defer cancel() - ret = invoker(newctx, method, req, reply, cc, opts...) - return - }) - conn, err := client.Dial(context.Background(), "127.0.0.1:8080") - if err != nil { - log.Error("did not connect: %v", err) - return - } - defer conn.Close() - - c := pb.NewGreeterClient(conn) - name := "2233" - rp, err := c.SayHello(context.Background(), &pb.HelloRequest{Name: name, Age: 18}) - if err != nil { - log.Error("could not greet: %v", err) - return - } - fmt.Println("rp", *rp) -} diff --git a/pkg/net/rpc/warden/internal/benchmark/bench/client/client.go b/pkg/net/rpc/warden/internal/benchmark/bench/client/client.go deleted file mode 100644 index b7bc5fc8b..000000000 --- a/pkg/net/rpc/warden/internal/benchmark/bench/client/client.go +++ /dev/null @@ -1,186 +0,0 @@ -package main - -import ( - "flag" - "log" - "reflect" - "sync" - "sync/atomic" - "time" - - "github.com/go-kratos/kratos/pkg/net/netutil/breaker" - "github.com/go-kratos/kratos/pkg/net/rpc/warden" - "github.com/go-kratos/kratos/pkg/net/rpc/warden/internal/benchmark/bench/proto" - xtime "github.com/go-kratos/kratos/pkg/time" - - goproto "github.com/gogo/protobuf/proto" - "github.com/montanaflynn/stats" - "golang.org/x/net/context" - "google.golang.org/grpc" -) - -const ( - iws = 65535 * 1000 - iwsc = 65535 * 10000 - readBuffer = 32 * 1024 - writeBuffer = 32 * 1024 -) - -var concurrency = flag.Int("c", 50, "concurrency") -var total = flag.Int("t", 500000, "total requests for all clients") -var host = flag.String("s", "127.0.0.1:8972", "server ip and port") -var isWarden = flag.Bool("w", true, "is warden or grpc client") -var strLen = flag.Int("l", 600, "the length of the str") - -func wardenCli() proto.HelloClient { - log.Println("start warden cli") - client := warden.NewClient(&warden.ClientConfig{ - Dial: xtime.Duration(time.Second * 10), - Timeout: xtime.Duration(time.Second * 10), - Breaker: &breaker.Config{ - Window: xtime.Duration(3 * time.Second), - Bucket: 10, - Request: 20, - K: 1.5, - }, - }, - grpc.WithInitialWindowSize(iws), - grpc.WithInitialConnWindowSize(iwsc), - grpc.WithReadBufferSize(readBuffer), - grpc.WithWriteBufferSize(writeBuffer)) - conn, err := client.Dial(context.Background(), *host) - if err != nil { - log.Fatalf("did not connect: %v", err) - } - cli := proto.NewHelloClient(conn) - return cli -} - -func grpcCli() proto.HelloClient { - log.Println("start grpc cli") - conn, err := grpc.Dial(*host, grpc.WithInsecure(), - grpc.WithInitialWindowSize(iws), - grpc.WithInitialConnWindowSize(iwsc), - grpc.WithReadBufferSize(readBuffer), - grpc.WithWriteBufferSize(writeBuffer)) - if err != nil { - log.Fatalf("did not connect: %v", err) - } - cli := proto.NewHelloClient(conn) - return cli -} - -func main() { - flag.Parse() - c := *concurrency - m := *total / c - var wg sync.WaitGroup - wg.Add(c) - log.Printf("concurrency: %d\nrequests per client: %d\n\n", c, m) - - args := prepareArgs() - b, _ := goproto.Marshal(args) - log.Printf("message size: %d bytes\n\n", len(b)) - - var trans uint64 - var transOK uint64 - d := make([][]int64, c) - for i := 0; i < c; i++ { - dt := make([]int64, 0, m) - d = append(d, dt) - } - var cli proto.HelloClient - if *isWarden { - cli = wardenCli() - } else { - cli = grpcCli() - } - //warmup - cli.Say(context.Background(), args) - - totalT := time.Now().UnixNano() - for i := 0; i < c; i++ { - go func(i int) { - for j := 0; j < m; j++ { - t := time.Now().UnixNano() - reply, err := cli.Say(context.Background(), args) - t = time.Now().UnixNano() - t - d[i] = append(d[i], t) - if err == nil && reply.Field1 == "OK" { - atomic.AddUint64(&transOK, 1) - } - atomic.AddUint64(&trans, 1) - } - wg.Done() - }(i) - } - wg.Wait() - - totalT = time.Now().UnixNano() - totalT - totalT = totalT / 1e6 - log.Printf("took %d ms for %d requests\n", totalT, *total) - totalD := make([]int64, 0, *total) - for _, k := range d { - totalD = append(totalD, k...) - } - totalD2 := make([]float64, 0, *total) - for _, k := range totalD { - totalD2 = append(totalD2, float64(k)) - } - - mean, _ := stats.Mean(totalD2) - median, _ := stats.Median(totalD2) - max, _ := stats.Max(totalD2) - min, _ := stats.Min(totalD2) - tp99, _ := stats.Percentile(totalD2, 99) - tp999, _ := stats.Percentile(totalD2, 99.9) - - log.Printf("sent requests : %d\n", *total) - log.Printf("received requests_OK : %d\n", atomic.LoadUint64(&transOK)) - log.Printf("throughput (TPS) : %d\n", int64(c*m)*1000/totalT) - log.Printf("mean: %v ms, median: %v ms, max: %v ms, min: %v ms, p99: %v ms, p999:%v ms\n", mean/1e6, median/1e6, max/1e6, min/1e6, tp99/1e6, tp999/1e6) -} - -func prepareArgs() *proto.BenchmarkMessage { - b := true - var i int32 = 120000 - var i64 int64 = 98765432101234 - var s = "许多往事在眼前一幕一幕,变的那麼模糊" - repeat := *strLen / (8 * 54) - if repeat == 0 { - repeat = 1 - } - var str string - for i := 0; i < repeat; i++ { - str += s - } - var args proto.BenchmarkMessage - - v := reflect.ValueOf(&args).Elem() - num := v.NumField() - for k := 0; k < num; k++ { - field := v.Field(k) - if field.Type().Kind() == reflect.Ptr { - switch v.Field(k).Type().Elem().Kind() { - case reflect.Int, reflect.Int32: - field.Set(reflect.ValueOf(&i)) - case reflect.Int64: - field.Set(reflect.ValueOf(&i64)) - case reflect.Bool: - field.Set(reflect.ValueOf(&b)) - case reflect.String: - field.Set(reflect.ValueOf(&str)) - } - } else { - switch field.Kind() { - case reflect.Int, reflect.Int32, reflect.Int64: - field.SetInt(9876543) - case reflect.Bool: - field.SetBool(true) - case reflect.String: - field.SetString(str) - } - } - } - return &args -} diff --git a/pkg/net/rpc/warden/internal/benchmark/bench/proto/hello.pb.go b/pkg/net/rpc/warden/internal/benchmark/bench/proto/hello.pb.go deleted file mode 100644 index 0bd9167ed..000000000 --- a/pkg/net/rpc/warden/internal/benchmark/bench/proto/hello.pb.go +++ /dev/null @@ -1,1686 +0,0 @@ -// Code generated by protoc-gen-gogo. DO NOT EDIT. -// source: hello.proto - -/* - Package grpc is a generated protocol buffer package. - - It is generated from these files: - hello.proto - - It has these top-level messages: - BenchmarkMessage -*/ -package proto - -import proto "github.com/golang/protobuf/proto" -import fmt "fmt" -import math "math" -import _ "github.com/gogo/protobuf/gogoproto" - -import context "golang.org/x/net/context" -import grpc1 "google.golang.org/grpc" - -import binary "encoding/binary" - -import io "io" - -// Reference imports to suppress errors if they are not otherwise used. -var _ = proto.Marshal -var _ = fmt.Errorf -var _ = math.Inf - -// This is a compile-time assertion to ensure that this generated file -// is compatible with the proto package it is being compiled against. -// A compilation error at this line likely means your copy of the -// proto package needs to be updated. -const _ = proto.ProtoPackageIsVersion2 // please upgrade the proto package - -type BenchmarkMessage struct { - Field1 string `protobuf:"bytes,1,opt,name=field1,proto3" json:"field1,omitempty"` - Field9 string `protobuf:"bytes,9,opt,name=field9,proto3" json:"field9,omitempty"` - Field18 string `protobuf:"bytes,18,opt,name=field18,proto3" json:"field18,omitempty"` - Field80 bool `protobuf:"varint,80,opt,name=field80,proto3" json:"field80,omitempty"` - Field81 bool `protobuf:"varint,81,opt,name=field81,proto3" json:"field81,omitempty"` - Field2 int32 `protobuf:"varint,2,opt,name=field2,proto3" json:"field2,omitempty"` - Field3 int32 `protobuf:"varint,3,opt,name=field3,proto3" json:"field3,omitempty"` - Field280 int32 `protobuf:"varint,280,opt,name=field280,proto3" json:"field280,omitempty"` - Field6 int32 `protobuf:"varint,6,opt,name=field6,proto3" json:"field6,omitempty"` - Field22 int64 `protobuf:"varint,22,opt,name=field22,proto3" json:"field22,omitempty"` - Field4 string `protobuf:"bytes,4,opt,name=field4,proto3" json:"field4,omitempty"` - Field5 uint64 `protobuf:"fixed64,5,opt,name=field5,proto3" json:"field5,omitempty"` - Field59 bool `protobuf:"varint,59,opt,name=field59,proto3" json:"field59,omitempty"` - Field7 string `protobuf:"bytes,7,opt,name=field7,proto3" json:"field7,omitempty"` - Field16 int32 `protobuf:"varint,16,opt,name=field16,proto3" json:"field16,omitempty"` - Field130 int32 `protobuf:"varint,130,opt,name=field130,proto3" json:"field130,omitempty"` - Field12 bool `protobuf:"varint,12,opt,name=field12,proto3" json:"field12,omitempty"` - Field17 bool `protobuf:"varint,17,opt,name=field17,proto3" json:"field17,omitempty"` - Field13 bool `protobuf:"varint,13,opt,name=field13,proto3" json:"field13,omitempty"` - Field14 bool `protobuf:"varint,14,opt,name=field14,proto3" json:"field14,omitempty"` - Field104 int32 `protobuf:"varint,104,opt,name=field104,proto3" json:"field104,omitempty"` - Field100 int32 `protobuf:"varint,100,opt,name=field100,proto3" json:"field100,omitempty"` - Field101 int32 `protobuf:"varint,101,opt,name=field101,proto3" json:"field101,omitempty"` - Field102 string `protobuf:"bytes,102,opt,name=field102,proto3" json:"field102,omitempty"` - Field103 string `protobuf:"bytes,103,opt,name=field103,proto3" json:"field103,omitempty"` - Field29 int32 `protobuf:"varint,29,opt,name=field29,proto3" json:"field29,omitempty"` - Field30 bool `protobuf:"varint,30,opt,name=field30,proto3" json:"field30,omitempty"` - Field60 int32 `protobuf:"varint,60,opt,name=field60,proto3" json:"field60,omitempty"` - Field271 int32 `protobuf:"varint,271,opt,name=field271,proto3" json:"field271,omitempty"` - Field272 int32 `protobuf:"varint,272,opt,name=field272,proto3" json:"field272,omitempty"` - Field150 int32 `protobuf:"varint,150,opt,name=field150,proto3" json:"field150,omitempty"` - Field23 int32 `protobuf:"varint,23,opt,name=field23,proto3" json:"field23,omitempty"` - Field24 bool `protobuf:"varint,24,opt,name=field24,proto3" json:"field24,omitempty"` - Field25 int32 `protobuf:"varint,25,opt,name=field25,proto3" json:"field25,omitempty"` - Field78 bool `protobuf:"varint,78,opt,name=field78,proto3" json:"field78,omitempty"` - Field67 int32 `protobuf:"varint,67,opt,name=field67,proto3" json:"field67,omitempty"` - Field68 int32 `protobuf:"varint,68,opt,name=field68,proto3" json:"field68,omitempty"` - Field128 int32 `protobuf:"varint,128,opt,name=field128,proto3" json:"field128,omitempty"` - Field129 string `protobuf:"bytes,129,opt,name=field129,proto3" json:"field129,omitempty"` - Field131 int32 `protobuf:"varint,131,opt,name=field131,proto3" json:"field131,omitempty"` -} - -func (m *BenchmarkMessage) Reset() { *m = BenchmarkMessage{} } -func (m *BenchmarkMessage) String() string { return proto.CompactTextString(m) } -func (*BenchmarkMessage) ProtoMessage() {} -func (*BenchmarkMessage) Descriptor() ([]byte, []int) { return fileDescriptorHello, []int{0} } - -func init() { - proto.RegisterType((*BenchmarkMessage)(nil), "grpc.BenchmarkMessage") -} - -// Reference imports to suppress errors if they are not otherwise used. -var _ context.Context -var _ grpc1.ClientConn - -// This is a compile-time assertion to ensure that this generated file -// is compatible with the grpc package it is being compiled against. -const _ = grpc1.SupportPackageIsVersion4 - -// Client API for Hello service - -type HelloClient interface { - // Sends a greeting - Say(ctx context.Context, in *BenchmarkMessage, opts ...grpc1.CallOption) (*BenchmarkMessage, error) -} - -type helloClient struct { - cc *grpc1.ClientConn -} - -func NewHelloClient(cc *grpc1.ClientConn) HelloClient { - return &helloClient{cc} -} - -func (c *helloClient) Say(ctx context.Context, in *BenchmarkMessage, opts ...grpc1.CallOption) (*BenchmarkMessage, error) { - out := new(BenchmarkMessage) - err := grpc1.Invoke(ctx, "/grpc.Hello/Say", in, out, c.cc, opts...) - if err != nil { - return nil, err - } - return out, nil -} - -// Server API for Hello service - -type HelloServer interface { - // Sends a greeting - Say(context.Context, *BenchmarkMessage) (*BenchmarkMessage, error) -} - -func RegisterHelloServer(s *grpc1.Server, srv HelloServer) { - s.RegisterService(&_Hello_serviceDesc, srv) -} - -func _Hello_Say_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc1.UnaryServerInterceptor) (interface{}, error) { - in := new(BenchmarkMessage) - if err := dec(in); err != nil { - return nil, err - } - if interceptor == nil { - return srv.(HelloServer).Say(ctx, in) - } - info := &grpc1.UnaryServerInfo{ - Server: srv, - FullMethod: "/grpc.Hello/Say", - } - handler := func(ctx context.Context, req interface{}) (interface{}, error) { - return srv.(HelloServer).Say(ctx, req.(*BenchmarkMessage)) - } - return interceptor(ctx, in, info, handler) -} - -var _Hello_serviceDesc = grpc1.ServiceDesc{ - ServiceName: "grpc.Hello", - HandlerType: (*HelloServer)(nil), - Methods: []grpc1.MethodDesc{ - { - MethodName: "Say", - Handler: _Hello_Say_Handler, - }, - }, - Streams: []grpc1.StreamDesc{}, - Metadata: "hello.proto", -} - -func (m *BenchmarkMessage) Marshal() (dAtA []byte, err error) { - size := m.Size() - dAtA = make([]byte, size) - n, err := m.MarshalTo(dAtA) - if err != nil { - return nil, err - } - return dAtA[:n], nil -} - -func (m *BenchmarkMessage) MarshalTo(dAtA []byte) (int, error) { - var i int - _ = i - var l int - _ = l - if len(m.Field1) > 0 { - dAtA[i] = 0xa - i++ - i = encodeVarintHello(dAtA, i, uint64(len(m.Field1))) - i += copy(dAtA[i:], m.Field1) - } - if m.Field2 != 0 { - dAtA[i] = 0x10 - i++ - i = encodeVarintHello(dAtA, i, uint64(m.Field2)) - } - if m.Field3 != 0 { - dAtA[i] = 0x18 - i++ - i = encodeVarintHello(dAtA, i, uint64(m.Field3)) - } - if len(m.Field4) > 0 { - dAtA[i] = 0x22 - i++ - i = encodeVarintHello(dAtA, i, uint64(len(m.Field4))) - i += copy(dAtA[i:], m.Field4) - } - if m.Field5 != 0 { - dAtA[i] = 0x29 - i++ - binary.LittleEndian.PutUint64(dAtA[i:], uint64(m.Field5)) - i += 8 - } - if m.Field6 != 0 { - dAtA[i] = 0x30 - i++ - i = encodeVarintHello(dAtA, i, uint64(m.Field6)) - } - if len(m.Field7) > 0 { - dAtA[i] = 0x3a - i++ - i = encodeVarintHello(dAtA, i, uint64(len(m.Field7))) - i += copy(dAtA[i:], m.Field7) - } - if len(m.Field9) > 0 { - dAtA[i] = 0x4a - i++ - i = encodeVarintHello(dAtA, i, uint64(len(m.Field9))) - i += copy(dAtA[i:], m.Field9) - } - if m.Field12 { - dAtA[i] = 0x60 - i++ - if m.Field12 { - dAtA[i] = 1 - } else { - dAtA[i] = 0 - } - i++ - } - if m.Field13 { - dAtA[i] = 0x68 - i++ - if m.Field13 { - dAtA[i] = 1 - } else { - dAtA[i] = 0 - } - i++ - } - if m.Field14 { - dAtA[i] = 0x70 - i++ - if m.Field14 { - dAtA[i] = 1 - } else { - dAtA[i] = 0 - } - i++ - } - if m.Field16 != 0 { - dAtA[i] = 0x80 - i++ - dAtA[i] = 0x1 - i++ - i = encodeVarintHello(dAtA, i, uint64(m.Field16)) - } - if m.Field17 { - dAtA[i] = 0x88 - i++ - dAtA[i] = 0x1 - i++ - if m.Field17 { - dAtA[i] = 1 - } else { - dAtA[i] = 0 - } - i++ - } - if len(m.Field18) > 0 { - dAtA[i] = 0x92 - i++ - dAtA[i] = 0x1 - i++ - i = encodeVarintHello(dAtA, i, uint64(len(m.Field18))) - i += copy(dAtA[i:], m.Field18) - } - if m.Field22 != 0 { - dAtA[i] = 0xb0 - i++ - dAtA[i] = 0x1 - i++ - i = encodeVarintHello(dAtA, i, uint64(m.Field22)) - } - if m.Field23 != 0 { - dAtA[i] = 0xb8 - i++ - dAtA[i] = 0x1 - i++ - i = encodeVarintHello(dAtA, i, uint64(m.Field23)) - } - if m.Field24 { - dAtA[i] = 0xc0 - i++ - dAtA[i] = 0x1 - i++ - if m.Field24 { - dAtA[i] = 1 - } else { - dAtA[i] = 0 - } - i++ - } - if m.Field25 != 0 { - dAtA[i] = 0xc8 - i++ - dAtA[i] = 0x1 - i++ - i = encodeVarintHello(dAtA, i, uint64(m.Field25)) - } - if m.Field29 != 0 { - dAtA[i] = 0xe8 - i++ - dAtA[i] = 0x1 - i++ - i = encodeVarintHello(dAtA, i, uint64(m.Field29)) - } - if m.Field30 { - dAtA[i] = 0xf0 - i++ - dAtA[i] = 0x1 - i++ - if m.Field30 { - dAtA[i] = 1 - } else { - dAtA[i] = 0 - } - i++ - } - if m.Field59 { - dAtA[i] = 0xd8 - i++ - dAtA[i] = 0x3 - i++ - if m.Field59 { - dAtA[i] = 1 - } else { - dAtA[i] = 0 - } - i++ - } - if m.Field60 != 0 { - dAtA[i] = 0xe0 - i++ - dAtA[i] = 0x3 - i++ - i = encodeVarintHello(dAtA, i, uint64(m.Field60)) - } - if m.Field67 != 0 { - dAtA[i] = 0x98 - i++ - dAtA[i] = 0x4 - i++ - i = encodeVarintHello(dAtA, i, uint64(m.Field67)) - } - if m.Field68 != 0 { - dAtA[i] = 0xa0 - i++ - dAtA[i] = 0x4 - i++ - i = encodeVarintHello(dAtA, i, uint64(m.Field68)) - } - if m.Field78 { - dAtA[i] = 0xf0 - i++ - dAtA[i] = 0x4 - i++ - if m.Field78 { - dAtA[i] = 1 - } else { - dAtA[i] = 0 - } - i++ - } - if m.Field80 { - dAtA[i] = 0x80 - i++ - dAtA[i] = 0x5 - i++ - if m.Field80 { - dAtA[i] = 1 - } else { - dAtA[i] = 0 - } - i++ - } - if m.Field81 { - dAtA[i] = 0x88 - i++ - dAtA[i] = 0x5 - i++ - if m.Field81 { - dAtA[i] = 1 - } else { - dAtA[i] = 0 - } - i++ - } - if m.Field100 != 0 { - dAtA[i] = 0xa0 - i++ - dAtA[i] = 0x6 - i++ - i = encodeVarintHello(dAtA, i, uint64(m.Field100)) - } - if m.Field101 != 0 { - dAtA[i] = 0xa8 - i++ - dAtA[i] = 0x6 - i++ - i = encodeVarintHello(dAtA, i, uint64(m.Field101)) - } - if len(m.Field102) > 0 { - dAtA[i] = 0xb2 - i++ - dAtA[i] = 0x6 - i++ - i = encodeVarintHello(dAtA, i, uint64(len(m.Field102))) - i += copy(dAtA[i:], m.Field102) - } - if len(m.Field103) > 0 { - dAtA[i] = 0xba - i++ - dAtA[i] = 0x6 - i++ - i = encodeVarintHello(dAtA, i, uint64(len(m.Field103))) - i += copy(dAtA[i:], m.Field103) - } - if m.Field104 != 0 { - dAtA[i] = 0xc0 - i++ - dAtA[i] = 0x6 - i++ - i = encodeVarintHello(dAtA, i, uint64(m.Field104)) - } - if m.Field128 != 0 { - dAtA[i] = 0x80 - i++ - dAtA[i] = 0x8 - i++ - i = encodeVarintHello(dAtA, i, uint64(m.Field128)) - } - if len(m.Field129) > 0 { - dAtA[i] = 0x8a - i++ - dAtA[i] = 0x8 - i++ - i = encodeVarintHello(dAtA, i, uint64(len(m.Field129))) - i += copy(dAtA[i:], m.Field129) - } - if m.Field130 != 0 { - dAtA[i] = 0x90 - i++ - dAtA[i] = 0x8 - i++ - i = encodeVarintHello(dAtA, i, uint64(m.Field130)) - } - if m.Field131 != 0 { - dAtA[i] = 0x98 - i++ - dAtA[i] = 0x8 - i++ - i = encodeVarintHello(dAtA, i, uint64(m.Field131)) - } - if m.Field150 != 0 { - dAtA[i] = 0xb0 - i++ - dAtA[i] = 0x9 - i++ - i = encodeVarintHello(dAtA, i, uint64(m.Field150)) - } - if m.Field271 != 0 { - dAtA[i] = 0xf8 - i++ - dAtA[i] = 0x10 - i++ - i = encodeVarintHello(dAtA, i, uint64(m.Field271)) - } - if m.Field272 != 0 { - dAtA[i] = 0x80 - i++ - dAtA[i] = 0x11 - i++ - i = encodeVarintHello(dAtA, i, uint64(m.Field272)) - } - if m.Field280 != 0 { - dAtA[i] = 0xc0 - i++ - dAtA[i] = 0x11 - i++ - i = encodeVarintHello(dAtA, i, uint64(m.Field280)) - } - return i, nil -} - -func encodeVarintHello(dAtA []byte, offset int, v uint64) int { - for v >= 1<<7 { - dAtA[offset] = uint8(v&0x7f | 0x80) - v >>= 7 - offset++ - } - dAtA[offset] = uint8(v) - return offset + 1 -} -func (m *BenchmarkMessage) Size() (n int) { - var l int - _ = l - l = len(m.Field1) - if l > 0 { - n += 1 + l + sovHello(uint64(l)) - } - if m.Field2 != 0 { - n += 1 + sovHello(uint64(m.Field2)) - } - if m.Field3 != 0 { - n += 1 + sovHello(uint64(m.Field3)) - } - l = len(m.Field4) - if l > 0 { - n += 1 + l + sovHello(uint64(l)) - } - if m.Field5 != 0 { - n += 9 - } - if m.Field6 != 0 { - n += 1 + sovHello(uint64(m.Field6)) - } - l = len(m.Field7) - if l > 0 { - n += 1 + l + sovHello(uint64(l)) - } - l = len(m.Field9) - if l > 0 { - n += 1 + l + sovHello(uint64(l)) - } - if m.Field12 { - n += 2 - } - if m.Field13 { - n += 2 - } - if m.Field14 { - n += 2 - } - if m.Field16 != 0 { - n += 2 + sovHello(uint64(m.Field16)) - } - if m.Field17 { - n += 3 - } - l = len(m.Field18) - if l > 0 { - n += 2 + l + sovHello(uint64(l)) - } - if m.Field22 != 0 { - n += 2 + sovHello(uint64(m.Field22)) - } - if m.Field23 != 0 { - n += 2 + sovHello(uint64(m.Field23)) - } - if m.Field24 { - n += 3 - } - if m.Field25 != 0 { - n += 2 + sovHello(uint64(m.Field25)) - } - if m.Field29 != 0 { - n += 2 + sovHello(uint64(m.Field29)) - } - if m.Field30 { - n += 3 - } - if m.Field59 { - n += 3 - } - if m.Field60 != 0 { - n += 2 + sovHello(uint64(m.Field60)) - } - if m.Field67 != 0 { - n += 2 + sovHello(uint64(m.Field67)) - } - if m.Field68 != 0 { - n += 2 + sovHello(uint64(m.Field68)) - } - if m.Field78 { - n += 3 - } - if m.Field80 { - n += 3 - } - if m.Field81 { - n += 3 - } - if m.Field100 != 0 { - n += 2 + sovHello(uint64(m.Field100)) - } - if m.Field101 != 0 { - n += 2 + sovHello(uint64(m.Field101)) - } - l = len(m.Field102) - if l > 0 { - n += 2 + l + sovHello(uint64(l)) - } - l = len(m.Field103) - if l > 0 { - n += 2 + l + sovHello(uint64(l)) - } - if m.Field104 != 0 { - n += 2 + sovHello(uint64(m.Field104)) - } - if m.Field128 != 0 { - n += 2 + sovHello(uint64(m.Field128)) - } - l = len(m.Field129) - if l > 0 { - n += 2 + l + sovHello(uint64(l)) - } - if m.Field130 != 0 { - n += 2 + sovHello(uint64(m.Field130)) - } - if m.Field131 != 0 { - n += 2 + sovHello(uint64(m.Field131)) - } - if m.Field150 != 0 { - n += 2 + sovHello(uint64(m.Field150)) - } - if m.Field271 != 0 { - n += 2 + sovHello(uint64(m.Field271)) - } - if m.Field272 != 0 { - n += 2 + sovHello(uint64(m.Field272)) - } - if m.Field280 != 0 { - n += 2 + sovHello(uint64(m.Field280)) - } - return n -} - -func sovHello(x uint64) (n int) { - for { - n++ - x >>= 7 - if x == 0 { - break - } - } - return n -} -func sozHello(x uint64) (n int) { - return sovHello(uint64((x << 1) ^ uint64((int64(x) >> 63)))) -} -func (m *BenchmarkMessage) Unmarshal(dAtA []byte) error { - l := len(dAtA) - iNdEx := 0 - for iNdEx < l { - preIndex := iNdEx - var wire uint64 - for shift := uint(0); ; shift += 7 { - if shift >= 64 { - return ErrIntOverflowHello - } - if iNdEx >= l { - return io.ErrUnexpectedEOF - } - b := dAtA[iNdEx] - iNdEx++ - wire |= (uint64(b) & 0x7F) << shift - if b < 0x80 { - break - } - } - fieldNum := int32(wire >> 3) - wireType := int(wire & 0x7) - if wireType == 4 { - return fmt.Errorf("proto: BenchmarkMessage: wiretype end group for non-group") - } - if fieldNum <= 0 { - return fmt.Errorf("proto: BenchmarkMessage: illegal tag %d (wire type %d)", fieldNum, wire) - } - switch fieldNum { - case 1: - if wireType != 2 { - return fmt.Errorf("proto: wrong wireType = %d for field Field1", wireType) - } - var stringLen uint64 - for shift := uint(0); ; shift += 7 { - if shift >= 64 { - return ErrIntOverflowHello - } - if iNdEx >= l { - return io.ErrUnexpectedEOF - } - b := dAtA[iNdEx] - iNdEx++ - stringLen |= (uint64(b) & 0x7F) << shift - if b < 0x80 { - break - } - } - intStringLen := int(stringLen) - if intStringLen < 0 { - return ErrInvalidLengthHello - } - postIndex := iNdEx + intStringLen - if postIndex > l { - return io.ErrUnexpectedEOF - } - m.Field1 = string(dAtA[iNdEx:postIndex]) - iNdEx = postIndex - case 2: - if wireType != 0 { - return fmt.Errorf("proto: wrong wireType = %d for field Field2", wireType) - } - m.Field2 = 0 - for shift := uint(0); ; shift += 7 { - if shift >= 64 { - return ErrIntOverflowHello - } - if iNdEx >= l { - return io.ErrUnexpectedEOF - } - b := dAtA[iNdEx] - iNdEx++ - m.Field2 |= (int32(b) & 0x7F) << shift - if b < 0x80 { - break - } - } - case 3: - if wireType != 0 { - return fmt.Errorf("proto: wrong wireType = %d for field Field3", wireType) - } - m.Field3 = 0 - for shift := uint(0); ; shift += 7 { - if shift >= 64 { - return ErrIntOverflowHello - } - if iNdEx >= l { - return io.ErrUnexpectedEOF - } - b := dAtA[iNdEx] - iNdEx++ - m.Field3 |= (int32(b) & 0x7F) << shift - if b < 0x80 { - break - } - } - case 4: - if wireType != 2 { - return fmt.Errorf("proto: wrong wireType = %d for field Field4", wireType) - } - var stringLen uint64 - for shift := uint(0); ; shift += 7 { - if shift >= 64 { - return ErrIntOverflowHello - } - if iNdEx >= l { - return io.ErrUnexpectedEOF - } - b := dAtA[iNdEx] - iNdEx++ - stringLen |= (uint64(b) & 0x7F) << shift - if b < 0x80 { - break - } - } - intStringLen := int(stringLen) - if intStringLen < 0 { - return ErrInvalidLengthHello - } - postIndex := iNdEx + intStringLen - if postIndex > l { - return io.ErrUnexpectedEOF - } - m.Field4 = string(dAtA[iNdEx:postIndex]) - iNdEx = postIndex - case 5: - if wireType != 1 { - return fmt.Errorf("proto: wrong wireType = %d for field Field5", wireType) - } - m.Field5 = 0 - if (iNdEx + 8) > l { - return io.ErrUnexpectedEOF - } - m.Field5 = uint64(binary.LittleEndian.Uint64(dAtA[iNdEx:])) - iNdEx += 8 - case 6: - if wireType != 0 { - return fmt.Errorf("proto: wrong wireType = %d for field Field6", wireType) - } - m.Field6 = 0 - for shift := uint(0); ; shift += 7 { - if shift >= 64 { - return ErrIntOverflowHello - } - if iNdEx >= l { - return io.ErrUnexpectedEOF - } - b := dAtA[iNdEx] - iNdEx++ - m.Field6 |= (int32(b) & 0x7F) << shift - if b < 0x80 { - break - } - } - case 7: - if wireType != 2 { - return fmt.Errorf("proto: wrong wireType = %d for field Field7", wireType) - } - var stringLen uint64 - for shift := uint(0); ; shift += 7 { - if shift >= 64 { - return ErrIntOverflowHello - } - if iNdEx >= l { - return io.ErrUnexpectedEOF - } - b := dAtA[iNdEx] - iNdEx++ - stringLen |= (uint64(b) & 0x7F) << shift - if b < 0x80 { - break - } - } - intStringLen := int(stringLen) - if intStringLen < 0 { - return ErrInvalidLengthHello - } - postIndex := iNdEx + intStringLen - if postIndex > l { - return io.ErrUnexpectedEOF - } - m.Field7 = string(dAtA[iNdEx:postIndex]) - iNdEx = postIndex - case 9: - if wireType != 2 { - return fmt.Errorf("proto: wrong wireType = %d for field Field9", wireType) - } - var stringLen uint64 - for shift := uint(0); ; shift += 7 { - if shift >= 64 { - return ErrIntOverflowHello - } - if iNdEx >= l { - return io.ErrUnexpectedEOF - } - b := dAtA[iNdEx] - iNdEx++ - stringLen |= (uint64(b) & 0x7F) << shift - if b < 0x80 { - break - } - } - intStringLen := int(stringLen) - if intStringLen < 0 { - return ErrInvalidLengthHello - } - postIndex := iNdEx + intStringLen - if postIndex > l { - return io.ErrUnexpectedEOF - } - m.Field9 = string(dAtA[iNdEx:postIndex]) - iNdEx = postIndex - case 12: - if wireType != 0 { - return fmt.Errorf("proto: wrong wireType = %d for field Field12", wireType) - } - var v int - for shift := uint(0); ; shift += 7 { - if shift >= 64 { - return ErrIntOverflowHello - } - if iNdEx >= l { - return io.ErrUnexpectedEOF - } - b := dAtA[iNdEx] - iNdEx++ - v |= (int(b) & 0x7F) << shift - if b < 0x80 { - break - } - } - m.Field12 = bool(v != 0) - case 13: - if wireType != 0 { - return fmt.Errorf("proto: wrong wireType = %d for field Field13", wireType) - } - var v int - for shift := uint(0); ; shift += 7 { - if shift >= 64 { - return ErrIntOverflowHello - } - if iNdEx >= l { - return io.ErrUnexpectedEOF - } - b := dAtA[iNdEx] - iNdEx++ - v |= (int(b) & 0x7F) << shift - if b < 0x80 { - break - } - } - m.Field13 = bool(v != 0) - case 14: - if wireType != 0 { - return fmt.Errorf("proto: wrong wireType = %d for field Field14", wireType) - } - var v int - for shift := uint(0); ; shift += 7 { - if shift >= 64 { - return ErrIntOverflowHello - } - if iNdEx >= l { - return io.ErrUnexpectedEOF - } - b := dAtA[iNdEx] - iNdEx++ - v |= (int(b) & 0x7F) << shift - if b < 0x80 { - break - } - } - m.Field14 = bool(v != 0) - case 16: - if wireType != 0 { - return fmt.Errorf("proto: wrong wireType = %d for field Field16", wireType) - } - m.Field16 = 0 - for shift := uint(0); ; shift += 7 { - if shift >= 64 { - return ErrIntOverflowHello - } - if iNdEx >= l { - return io.ErrUnexpectedEOF - } - b := dAtA[iNdEx] - iNdEx++ - m.Field16 |= (int32(b) & 0x7F) << shift - if b < 0x80 { - break - } - } - case 17: - if wireType != 0 { - return fmt.Errorf("proto: wrong wireType = %d for field Field17", wireType) - } - var v int - for shift := uint(0); ; shift += 7 { - if shift >= 64 { - return ErrIntOverflowHello - } - if iNdEx >= l { - return io.ErrUnexpectedEOF - } - b := dAtA[iNdEx] - iNdEx++ - v |= (int(b) & 0x7F) << shift - if b < 0x80 { - break - } - } - m.Field17 = bool(v != 0) - case 18: - if wireType != 2 { - return fmt.Errorf("proto: wrong wireType = %d for field Field18", wireType) - } - var stringLen uint64 - for shift := uint(0); ; shift += 7 { - if shift >= 64 { - return ErrIntOverflowHello - } - if iNdEx >= l { - return io.ErrUnexpectedEOF - } - b := dAtA[iNdEx] - iNdEx++ - stringLen |= (uint64(b) & 0x7F) << shift - if b < 0x80 { - break - } - } - intStringLen := int(stringLen) - if intStringLen < 0 { - return ErrInvalidLengthHello - } - postIndex := iNdEx + intStringLen - if postIndex > l { - return io.ErrUnexpectedEOF - } - m.Field18 = string(dAtA[iNdEx:postIndex]) - iNdEx = postIndex - case 22: - if wireType != 0 { - return fmt.Errorf("proto: wrong wireType = %d for field Field22", wireType) - } - m.Field22 = 0 - for shift := uint(0); ; shift += 7 { - if shift >= 64 { - return ErrIntOverflowHello - } - if iNdEx >= l { - return io.ErrUnexpectedEOF - } - b := dAtA[iNdEx] - iNdEx++ - m.Field22 |= (int64(b) & 0x7F) << shift - if b < 0x80 { - break - } - } - case 23: - if wireType != 0 { - return fmt.Errorf("proto: wrong wireType = %d for field Field23", wireType) - } - m.Field23 = 0 - for shift := uint(0); ; shift += 7 { - if shift >= 64 { - return ErrIntOverflowHello - } - if iNdEx >= l { - return io.ErrUnexpectedEOF - } - b := dAtA[iNdEx] - iNdEx++ - m.Field23 |= (int32(b) & 0x7F) << shift - if b < 0x80 { - break - } - } - case 24: - if wireType != 0 { - return fmt.Errorf("proto: wrong wireType = %d for field Field24", wireType) - } - var v int - for shift := uint(0); ; shift += 7 { - if shift >= 64 { - return ErrIntOverflowHello - } - if iNdEx >= l { - return io.ErrUnexpectedEOF - } - b := dAtA[iNdEx] - iNdEx++ - v |= (int(b) & 0x7F) << shift - if b < 0x80 { - break - } - } - m.Field24 = bool(v != 0) - case 25: - if wireType != 0 { - return fmt.Errorf("proto: wrong wireType = %d for field Field25", wireType) - } - m.Field25 = 0 - for shift := uint(0); ; shift += 7 { - if shift >= 64 { - return ErrIntOverflowHello - } - if iNdEx >= l { - return io.ErrUnexpectedEOF - } - b := dAtA[iNdEx] - iNdEx++ - m.Field25 |= (int32(b) & 0x7F) << shift - if b < 0x80 { - break - } - } - case 29: - if wireType != 0 { - return fmt.Errorf("proto: wrong wireType = %d for field Field29", wireType) - } - m.Field29 = 0 - for shift := uint(0); ; shift += 7 { - if shift >= 64 { - return ErrIntOverflowHello - } - if iNdEx >= l { - return io.ErrUnexpectedEOF - } - b := dAtA[iNdEx] - iNdEx++ - m.Field29 |= (int32(b) & 0x7F) << shift - if b < 0x80 { - break - } - } - case 30: - if wireType != 0 { - return fmt.Errorf("proto: wrong wireType = %d for field Field30", wireType) - } - var v int - for shift := uint(0); ; shift += 7 { - if shift >= 64 { - return ErrIntOverflowHello - } - if iNdEx >= l { - return io.ErrUnexpectedEOF - } - b := dAtA[iNdEx] - iNdEx++ - v |= (int(b) & 0x7F) << shift - if b < 0x80 { - break - } - } - m.Field30 = bool(v != 0) - case 59: - if wireType != 0 { - return fmt.Errorf("proto: wrong wireType = %d for field Field59", wireType) - } - var v int - for shift := uint(0); ; shift += 7 { - if shift >= 64 { - return ErrIntOverflowHello - } - if iNdEx >= l { - return io.ErrUnexpectedEOF - } - b := dAtA[iNdEx] - iNdEx++ - v |= (int(b) & 0x7F) << shift - if b < 0x80 { - break - } - } - m.Field59 = bool(v != 0) - case 60: - if wireType != 0 { - return fmt.Errorf("proto: wrong wireType = %d for field Field60", wireType) - } - m.Field60 = 0 - for shift := uint(0); ; shift += 7 { - if shift >= 64 { - return ErrIntOverflowHello - } - if iNdEx >= l { - return io.ErrUnexpectedEOF - } - b := dAtA[iNdEx] - iNdEx++ - m.Field60 |= (int32(b) & 0x7F) << shift - if b < 0x80 { - break - } - } - case 67: - if wireType != 0 { - return fmt.Errorf("proto: wrong wireType = %d for field Field67", wireType) - } - m.Field67 = 0 - for shift := uint(0); ; shift += 7 { - if shift >= 64 { - return ErrIntOverflowHello - } - if iNdEx >= l { - return io.ErrUnexpectedEOF - } - b := dAtA[iNdEx] - iNdEx++ - m.Field67 |= (int32(b) & 0x7F) << shift - if b < 0x80 { - break - } - } - case 68: - if wireType != 0 { - return fmt.Errorf("proto: wrong wireType = %d for field Field68", wireType) - } - m.Field68 = 0 - for shift := uint(0); ; shift += 7 { - if shift >= 64 { - return ErrIntOverflowHello - } - if iNdEx >= l { - return io.ErrUnexpectedEOF - } - b := dAtA[iNdEx] - iNdEx++ - m.Field68 |= (int32(b) & 0x7F) << shift - if b < 0x80 { - break - } - } - case 78: - if wireType != 0 { - return fmt.Errorf("proto: wrong wireType = %d for field Field78", wireType) - } - var v int - for shift := uint(0); ; shift += 7 { - if shift >= 64 { - return ErrIntOverflowHello - } - if iNdEx >= l { - return io.ErrUnexpectedEOF - } - b := dAtA[iNdEx] - iNdEx++ - v |= (int(b) & 0x7F) << shift - if b < 0x80 { - break - } - } - m.Field78 = bool(v != 0) - case 80: - if wireType != 0 { - return fmt.Errorf("proto: wrong wireType = %d for field Field80", wireType) - } - var v int - for shift := uint(0); ; shift += 7 { - if shift >= 64 { - return ErrIntOverflowHello - } - if iNdEx >= l { - return io.ErrUnexpectedEOF - } - b := dAtA[iNdEx] - iNdEx++ - v |= (int(b) & 0x7F) << shift - if b < 0x80 { - break - } - } - m.Field80 = bool(v != 0) - case 81: - if wireType != 0 { - return fmt.Errorf("proto: wrong wireType = %d for field Field81", wireType) - } - var v int - for shift := uint(0); ; shift += 7 { - if shift >= 64 { - return ErrIntOverflowHello - } - if iNdEx >= l { - return io.ErrUnexpectedEOF - } - b := dAtA[iNdEx] - iNdEx++ - v |= (int(b) & 0x7F) << shift - if b < 0x80 { - break - } - } - m.Field81 = bool(v != 0) - case 100: - if wireType != 0 { - return fmt.Errorf("proto: wrong wireType = %d for field Field100", wireType) - } - m.Field100 = 0 - for shift := uint(0); ; shift += 7 { - if shift >= 64 { - return ErrIntOverflowHello - } - if iNdEx >= l { - return io.ErrUnexpectedEOF - } - b := dAtA[iNdEx] - iNdEx++ - m.Field100 |= (int32(b) & 0x7F) << shift - if b < 0x80 { - break - } - } - case 101: - if wireType != 0 { - return fmt.Errorf("proto: wrong wireType = %d for field Field101", wireType) - } - m.Field101 = 0 - for shift := uint(0); ; shift += 7 { - if shift >= 64 { - return ErrIntOverflowHello - } - if iNdEx >= l { - return io.ErrUnexpectedEOF - } - b := dAtA[iNdEx] - iNdEx++ - m.Field101 |= (int32(b) & 0x7F) << shift - if b < 0x80 { - break - } - } - case 102: - if wireType != 2 { - return fmt.Errorf("proto: wrong wireType = %d for field Field102", wireType) - } - var stringLen uint64 - for shift := uint(0); ; shift += 7 { - if shift >= 64 { - return ErrIntOverflowHello - } - if iNdEx >= l { - return io.ErrUnexpectedEOF - } - b := dAtA[iNdEx] - iNdEx++ - stringLen |= (uint64(b) & 0x7F) << shift - if b < 0x80 { - break - } - } - intStringLen := int(stringLen) - if intStringLen < 0 { - return ErrInvalidLengthHello - } - postIndex := iNdEx + intStringLen - if postIndex > l { - return io.ErrUnexpectedEOF - } - m.Field102 = string(dAtA[iNdEx:postIndex]) - iNdEx = postIndex - case 103: - if wireType != 2 { - return fmt.Errorf("proto: wrong wireType = %d for field Field103", wireType) - } - var stringLen uint64 - for shift := uint(0); ; shift += 7 { - if shift >= 64 { - return ErrIntOverflowHello - } - if iNdEx >= l { - return io.ErrUnexpectedEOF - } - b := dAtA[iNdEx] - iNdEx++ - stringLen |= (uint64(b) & 0x7F) << shift - if b < 0x80 { - break - } - } - intStringLen := int(stringLen) - if intStringLen < 0 { - return ErrInvalidLengthHello - } - postIndex := iNdEx + intStringLen - if postIndex > l { - return io.ErrUnexpectedEOF - } - m.Field103 = string(dAtA[iNdEx:postIndex]) - iNdEx = postIndex - case 104: - if wireType != 0 { - return fmt.Errorf("proto: wrong wireType = %d for field Field104", wireType) - } - m.Field104 = 0 - for shift := uint(0); ; shift += 7 { - if shift >= 64 { - return ErrIntOverflowHello - } - if iNdEx >= l { - return io.ErrUnexpectedEOF - } - b := dAtA[iNdEx] - iNdEx++ - m.Field104 |= (int32(b) & 0x7F) << shift - if b < 0x80 { - break - } - } - case 128: - if wireType != 0 { - return fmt.Errorf("proto: wrong wireType = %d for field Field128", wireType) - } - m.Field128 = 0 - for shift := uint(0); ; shift += 7 { - if shift >= 64 { - return ErrIntOverflowHello - } - if iNdEx >= l { - return io.ErrUnexpectedEOF - } - b := dAtA[iNdEx] - iNdEx++ - m.Field128 |= (int32(b) & 0x7F) << shift - if b < 0x80 { - break - } - } - case 129: - if wireType != 2 { - return fmt.Errorf("proto: wrong wireType = %d for field Field129", wireType) - } - var stringLen uint64 - for shift := uint(0); ; shift += 7 { - if shift >= 64 { - return ErrIntOverflowHello - } - if iNdEx >= l { - return io.ErrUnexpectedEOF - } - b := dAtA[iNdEx] - iNdEx++ - stringLen |= (uint64(b) & 0x7F) << shift - if b < 0x80 { - break - } - } - intStringLen := int(stringLen) - if intStringLen < 0 { - return ErrInvalidLengthHello - } - postIndex := iNdEx + intStringLen - if postIndex > l { - return io.ErrUnexpectedEOF - } - m.Field129 = string(dAtA[iNdEx:postIndex]) - iNdEx = postIndex - case 130: - if wireType != 0 { - return fmt.Errorf("proto: wrong wireType = %d for field Field130", wireType) - } - m.Field130 = 0 - for shift := uint(0); ; shift += 7 { - if shift >= 64 { - return ErrIntOverflowHello - } - if iNdEx >= l { - return io.ErrUnexpectedEOF - } - b := dAtA[iNdEx] - iNdEx++ - m.Field130 |= (int32(b) & 0x7F) << shift - if b < 0x80 { - break - } - } - case 131: - if wireType != 0 { - return fmt.Errorf("proto: wrong wireType = %d for field Field131", wireType) - } - m.Field131 = 0 - for shift := uint(0); ; shift += 7 { - if shift >= 64 { - return ErrIntOverflowHello - } - if iNdEx >= l { - return io.ErrUnexpectedEOF - } - b := dAtA[iNdEx] - iNdEx++ - m.Field131 |= (int32(b) & 0x7F) << shift - if b < 0x80 { - break - } - } - case 150: - if wireType != 0 { - return fmt.Errorf("proto: wrong wireType = %d for field Field150", wireType) - } - m.Field150 = 0 - for shift := uint(0); ; shift += 7 { - if shift >= 64 { - return ErrIntOverflowHello - } - if iNdEx >= l { - return io.ErrUnexpectedEOF - } - b := dAtA[iNdEx] - iNdEx++ - m.Field150 |= (int32(b) & 0x7F) << shift - if b < 0x80 { - break - } - } - case 271: - if wireType != 0 { - return fmt.Errorf("proto: wrong wireType = %d for field Field271", wireType) - } - m.Field271 = 0 - for shift := uint(0); ; shift += 7 { - if shift >= 64 { - return ErrIntOverflowHello - } - if iNdEx >= l { - return io.ErrUnexpectedEOF - } - b := dAtA[iNdEx] - iNdEx++ - m.Field271 |= (int32(b) & 0x7F) << shift - if b < 0x80 { - break - } - } - case 272: - if wireType != 0 { - return fmt.Errorf("proto: wrong wireType = %d for field Field272", wireType) - } - m.Field272 = 0 - for shift := uint(0); ; shift += 7 { - if shift >= 64 { - return ErrIntOverflowHello - } - if iNdEx >= l { - return io.ErrUnexpectedEOF - } - b := dAtA[iNdEx] - iNdEx++ - m.Field272 |= (int32(b) & 0x7F) << shift - if b < 0x80 { - break - } - } - case 280: - if wireType != 0 { - return fmt.Errorf("proto: wrong wireType = %d for field Field280", wireType) - } - m.Field280 = 0 - for shift := uint(0); ; shift += 7 { - if shift >= 64 { - return ErrIntOverflowHello - } - if iNdEx >= l { - return io.ErrUnexpectedEOF - } - b := dAtA[iNdEx] - iNdEx++ - m.Field280 |= (int32(b) & 0x7F) << shift - if b < 0x80 { - break - } - } - default: - iNdEx = preIndex - skippy, err := skipHello(dAtA[iNdEx:]) - if err != nil { - return err - } - if skippy < 0 { - return ErrInvalidLengthHello - } - if (iNdEx + skippy) > l { - return io.ErrUnexpectedEOF - } - iNdEx += skippy - } - } - - if iNdEx > l { - return io.ErrUnexpectedEOF - } - return nil -} -func skipHello(dAtA []byte) (n int, err error) { - l := len(dAtA) - iNdEx := 0 - for iNdEx < l { - var wire uint64 - for shift := uint(0); ; shift += 7 { - if shift >= 64 { - return 0, ErrIntOverflowHello - } - if iNdEx >= l { - return 0, io.ErrUnexpectedEOF - } - b := dAtA[iNdEx] - iNdEx++ - wire |= (uint64(b) & 0x7F) << shift - if b < 0x80 { - break - } - } - wireType := int(wire & 0x7) - switch wireType { - case 0: - for shift := uint(0); ; shift += 7 { - if shift >= 64 { - return 0, ErrIntOverflowHello - } - if iNdEx >= l { - return 0, io.ErrUnexpectedEOF - } - iNdEx++ - if dAtA[iNdEx-1] < 0x80 { - break - } - } - return iNdEx, nil - case 1: - iNdEx += 8 - return iNdEx, nil - case 2: - var length int - for shift := uint(0); ; shift += 7 { - if shift >= 64 { - return 0, ErrIntOverflowHello - } - if iNdEx >= l { - return 0, io.ErrUnexpectedEOF - } - b := dAtA[iNdEx] - iNdEx++ - length |= (int(b) & 0x7F) << shift - if b < 0x80 { - break - } - } - iNdEx += length - if length < 0 { - return 0, ErrInvalidLengthHello - } - return iNdEx, nil - case 3: - for { - var innerWire uint64 - var start int = iNdEx - for shift := uint(0); ; shift += 7 { - if shift >= 64 { - return 0, ErrIntOverflowHello - } - if iNdEx >= l { - return 0, io.ErrUnexpectedEOF - } - b := dAtA[iNdEx] - iNdEx++ - innerWire |= (uint64(b) & 0x7F) << shift - if b < 0x80 { - break - } - } - innerWireType := int(innerWire & 0x7) - if innerWireType == 4 { - break - } - next, err := skipHello(dAtA[start:]) - if err != nil { - return 0, err - } - iNdEx = start + next - } - return iNdEx, nil - case 4: - return iNdEx, nil - case 5: - iNdEx += 4 - return iNdEx, nil - default: - return 0, fmt.Errorf("proto: illegal wireType %d", wireType) - } - } - panic("unreachable") -} - -var ( - ErrInvalidLengthHello = fmt.Errorf("proto: negative length found during unmarshaling") - ErrIntOverflowHello = fmt.Errorf("proto: integer overflow") -) - -func init() { proto.RegisterFile("hello.proto", fileDescriptorHello) } - -var fileDescriptorHello = []byte{ - // 495 bytes of a gzipped FileDescriptorProto - 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0x74, 0xd4, 0x4d, 0x6e, 0xd3, 0x40, - 0x14, 0x07, 0xf0, 0x4c, 0xdb, 0xa4, 0xa9, 0xf9, 0x50, 0xf1, 0x22, 0xfc, 0x09, 0xc2, 0x8a, 0xba, - 0xca, 0x86, 0x74, 0x3c, 0x63, 0x7b, 0xc6, 0x82, 0x05, 0x2a, 0x2c, 0xba, 0x01, 0x41, 0x38, 0x41, - 0x92, 0x3a, 0x4e, 0x45, 0x8a, 0xab, 0xb4, 0x5d, 0xb0, 0xe3, 0xe3, 0x00, 0xb0, 0x42, 0x3d, 0x08, - 0x87, 0xe8, 0xb2, 0x47, 0xa0, 0xe1, 0x22, 0xa8, 0x4e, 0x3c, 0xef, 0x19, 0x89, 0x9d, 0xdf, 0xff, - 0xa7, 0xf7, 0xe6, 0x69, 0x2c, 0x8d, 0x77, 0x67, 0x96, 0xcd, 0xe7, 0xc5, 0xe0, 0x74, 0x51, 0x9c, - 0x17, 0xfe, 0x56, 0xbe, 0x38, 0x9d, 0x74, 0x9f, 0xe6, 0xc7, 0xe7, 0xb3, 0x8b, 0xf1, 0x60, 0x52, - 0x9c, 0xec, 0xe7, 0x45, 0x5e, 0xec, 0x97, 0x38, 0xbe, 0x98, 0x96, 0x55, 0x59, 0x94, 0x5f, 0xab, - 0xa6, 0xbd, 0x5f, 0x6d, 0x6f, 0xf7, 0x20, 0xfb, 0x38, 0x99, 0x9d, 0x8c, 0x16, 0x1f, 0x5e, 0x67, - 0x67, 0x67, 0xa3, 0x3c, 0xf3, 0x3b, 0x5e, 0x6b, 0x7a, 0x9c, 0xcd, 0x8f, 0x42, 0x88, 0x9e, 0xe8, - 0xef, 0x0c, 0xd7, 0x95, 0xcb, 0x15, 0x36, 0x7a, 0xa2, 0xdf, 0x5c, 0xe7, 0xca, 0xe5, 0x1a, 0x9b, - 0x2c, 0xd7, 0x2e, 0x8f, 0xb0, 0xc5, 0xe6, 0x44, 0x2e, 0x8f, 0xd1, 0xec, 0x89, 0x7e, 0x6b, 0x9d, - 0xc7, 0x2e, 0x4f, 0xd0, 0x62, 0x73, 0x12, 0x97, 0x1b, 0x6c, 0xb3, 0x39, 0xc6, 0xe5, 0x29, 0x76, - 0x58, 0x9e, 0xfa, 0xf0, 0xb6, 0x57, 0x1b, 0x2b, 0xdc, 0xed, 0x89, 0x7e, 0x7b, 0x58, 0x95, 0x24, - 0x1a, 0xf7, 0xb8, 0x68, 0x92, 0x08, 0xf7, 0xb9, 0x44, 0x24, 0x09, 0x76, 0xcb, 0xb5, 0xaa, 0x92, - 0xc4, 0xe0, 0x01, 0xef, 0x31, 0x24, 0x16, 0x7e, 0xb9, 0x5a, 0x55, 0x3a, 0x51, 0x0a, 0x9d, 0x9e, - 0xe8, 0x6f, 0x0e, 0xab, 0x92, 0x44, 0xe3, 0x21, 0x3b, 0x47, 0xd1, 0x6e, 0x2a, 0x02, 0xd8, 0x39, - 0x8a, 0x76, 0x53, 0x31, 0x1e, 0xf1, 0x9e, 0x98, 0x24, 0xc5, 0x13, 0x2e, 0x74, 0x3b, 0x5a, 0x22, - 0x60, 0xd3, 0xb4, 0x74, 0x12, 0xa7, 0x78, 0xc6, 0x24, 0xa6, 0x9e, 0x44, 0xe2, 0x39, 0x9b, 0x96, - 0x50, 0x4f, 0x62, 0xf0, 0x92, 0x0b, 0xdd, 0x41, 0x62, 0xf1, 0x8a, 0x0b, 0xdd, 0x81, 0xb1, 0x78, - 0xc3, 0xce, 0x31, 0x24, 0x56, 0xe2, 0x2d, 0x13, 0x4b, 0xe7, 0xd8, 0x10, 0xef, 0xb8, 0x84, 0x7e, - 0xd7, 0x6b, 0xaf, 0x2e, 0x57, 0x4a, 0x1c, 0x95, 0x07, 0xb9, 0x9a, 0x59, 0x88, 0xac, 0x66, 0xbc, - 0x4f, 0x61, 0x5a, 0xfe, 0x24, 0x57, 0x33, 0xd3, 0xc8, 0x6b, 0xa6, 0x99, 0x45, 0x98, 0xd5, 0x66, - 0x46, 0xfe, 0xe3, 0xca, 0x94, 0xc5, 0x67, 0xc1, 0x51, 0x59, 0x86, 0x29, 0xbe, 0x08, 0x3e, 0x55, - 0xa5, 0x84, 0x5a, 0xe2, 0x6b, 0xad, 0x53, 0x4b, 0x86, 0x21, 0xbe, 0xd5, 0x31, 0x24, 0x8c, 0x25, - 0x7e, 0xd6, 0x30, 0xa6, 0x4e, 0x65, 0x42, 0x7c, 0xdf, 0x60, 0xa8, 0x4c, 0xc8, 0x50, 0xe1, 0x47, - 0x1d, 0x15, 0xa1, 0x95, 0xb8, 0xac, 0xa1, 0x95, 0xea, 0x85, 0xd7, 0x3c, 0xbc, 0x7d, 0x7a, 0x7c, - 0xe3, 0x6d, 0xbe, 0x1f, 0x7d, 0xf2, 0x3b, 0x83, 0xdb, 0xc7, 0x67, 0xf0, 0xef, 0x4b, 0xd2, 0xfd, - 0x4f, 0xbe, 0xd7, 0x38, 0xe8, 0x5e, 0xdd, 0x04, 0x8d, 0xeb, 0x9b, 0xa0, 0x71, 0xb5, 0x0c, 0xc4, - 0xf5, 0x32, 0x10, 0xbf, 0x97, 0x81, 0xb8, 0xfc, 0x13, 0x34, 0x0e, 0xc5, 0xb8, 0x55, 0xbe, 0x4d, - 0xfa, 0x6f, 0x00, 0x00, 0x00, 0xff, 0xff, 0xf2, 0xed, 0x6f, 0xb7, 0xdf, 0x04, 0x00, 0x00, -} diff --git a/pkg/net/rpc/warden/internal/benchmark/bench/proto/hello.proto b/pkg/net/rpc/warden/internal/benchmark/bench/proto/hello.proto deleted file mode 100644 index cb40908ca..000000000 --- a/pkg/net/rpc/warden/internal/benchmark/bench/proto/hello.proto +++ /dev/null @@ -1,60 +0,0 @@ -syntax = "proto3"; -package proto; - -import "github.com/gogo/protobuf/gogoproto/gogo.proto"; - -option optimize_for = SPEED; -option (gogoproto.goproto_enum_prefix_all) = false; -option (gogoproto.goproto_getters_all) = false; -option (gogoproto.unmarshaler_all) = true; -option (gogoproto.marshaler_all) = true; -option (gogoproto.sizer_all) = true; - -service Hello { - // Sends a greeting - rpc Say (BenchmarkMessage) returns (BenchmarkMessage) {} -} - - -message BenchmarkMessage { - string field1 = 1; - string field9 = 9; - string field18 = 18; - bool field80 = 80; - bool field81 = 81; - int32 field2 = 2; - int32 field3 = 3; - int32 field280 = 280; - int32 field6 = 6; - int64 field22 = 22; - string field4 = 4; - fixed64 field5 = 5; - bool field59 = 59; - string field7 = 7; - int32 field16 = 16; - int32 field130 = 130; - bool field12 = 12; - bool field17 = 17; - bool field13 = 13; - bool field14 = 14; - int32 field104 = 104; - int32 field100 = 100; - int32 field101 = 101; - string field102 = 102; - string field103 = 103; - int32 field29 = 29; - bool field30 = 30; - int32 field60 = 60; - int32 field271 = 271; - int32 field272 = 272; - int32 field150 = 150; - int32 field23 = 23; - bool field24 = 24 ; - int32 field25 = 25 ; - bool field78 = 78; - int32 field67 = 67; - int32 field68 = 68; - int32 field128 = 128; - string field129 = 129; - int32 field131 = 131; -} \ No newline at end of file diff --git a/pkg/net/rpc/warden/internal/benchmark/bench/server/server.go b/pkg/net/rpc/warden/internal/benchmark/bench/server/server.go deleted file mode 100644 index 755718eed..000000000 --- a/pkg/net/rpc/warden/internal/benchmark/bench/server/server.go +++ /dev/null @@ -1,103 +0,0 @@ -package main - -import ( - "context" - "flag" - "log" - "net" - "net/http" - _ "net/http/pprof" - "sync/atomic" - "time" - - "github.com/go-kratos/kratos/pkg/net/rpc/warden" - "github.com/go-kratos/kratos/pkg/net/rpc/warden/internal/benchmark/bench/proto" - xtime "github.com/go-kratos/kratos/pkg/time" - - "github.com/prometheus/client_golang/prometheus/promhttp" - "google.golang.org/grpc" -) - -const ( - iws = 65535 * 1000 - iwsc = 65535 * 10000 - readBuffer = 32 * 1024 - writeBuffer = 32 * 1024 -) - -var reqNum uint64 - -type Hello struct{} - -func (t *Hello) Say(ctx context.Context, args *proto.BenchmarkMessage) (reply *proto.BenchmarkMessage, err error) { - s := "OK" - var i int32 = 100 - args.Field1 = s - args.Field2 = i - atomic.AddUint64(&reqNum, 1) - return args, nil -} - -var host = flag.String("s", "0.0.0.0:8972", "listened ip and port") -var isWarden = flag.Bool("w", true, "is warden or grpc client") - -func main() { - go func() { - log.Println("run http at :6060") - http.HandleFunc("/metrics", func(w http.ResponseWriter, r *http.Request) { - h := promhttp.Handler() - h.ServeHTTP(w, r) - }) - log.Println(http.ListenAndServe("0.0.0.0:6060", nil)) - }() - - flag.Parse() - - go stat() - if *isWarden { - runWarden() - } else { - runGrpc() - } -} - -func runGrpc() { - log.Println("run grpc") - lis, err := net.Listen("tcp", *host) - if err != nil { - log.Fatalf("failed to listen: %v", err) - } - s := grpc.NewServer(grpc.InitialWindowSize(iws), - grpc.InitialConnWindowSize(iwsc), - grpc.ReadBufferSize(readBuffer), - grpc.WriteBufferSize(writeBuffer)) - proto.RegisterHelloServer(s, &Hello{}) - s.Serve(lis) -} - -func runWarden() { - log.Println("run warden") - s := warden.NewServer(&warden.ServerConfig{Timeout: xtime.Duration(time.Second * 3)}, - grpc.InitialWindowSize(iws), - grpc.InitialConnWindowSize(iwsc), - grpc.ReadBufferSize(readBuffer), - grpc.WriteBufferSize(writeBuffer)) - proto.RegisterHelloServer(s.Server(), &Hello{}) - s.Run(*host) -} - -func stat() { - ticker := time.NewTicker(time.Second * 5) - defer ticker.Stop() - var last uint64 - lastTs := uint64(time.Now().UnixNano()) - for { - <-ticker.C - now := atomic.LoadUint64(&reqNum) - nowTs := uint64(time.Now().UnixNano()) - qps := (now - last) * 1e6 / ((nowTs - lastTs) / 1e3) - last = now - lastTs = nowTs - log.Println("qps:", qps) - } -} diff --git a/pkg/net/rpc/warden/internal/benchmark/helloworld/client.sh b/pkg/net/rpc/warden/internal/benchmark/helloworld/client.sh deleted file mode 100755 index 3490e7139..000000000 --- a/pkg/net/rpc/warden/internal/benchmark/helloworld/client.sh +++ /dev/null @@ -1,15 +0,0 @@ -#!/bin/bash -go build -o client greeter_client.go -echo size 100 concurrent 30 -./client -s 100 -c 30 -echo size 1000 concurrent 30 -./client -s 1000 -c 30 -echo size 10000 concurrent 30 -./client -s 10000 -c 30 -echo size 100 concurrent 300 -./client -s 100 -c 300 -echo size 1000 concurrent 300 -./client -s 1000 -c 300 -echo size 10000 concurrent 300 -./client -s 10000 -c 300 -rm client \ No newline at end of file diff --git a/pkg/net/rpc/warden/internal/benchmark/helloworld/client/greeter_client.go b/pkg/net/rpc/warden/internal/benchmark/helloworld/client/greeter_client.go deleted file mode 100644 index fad9a8d6d..000000000 --- a/pkg/net/rpc/warden/internal/benchmark/helloworld/client/greeter_client.go +++ /dev/null @@ -1,83 +0,0 @@ -package main - -import ( - "context" - "flag" - "fmt" - "math/rand" - "sync" - "sync/atomic" - "time" - - "github.com/go-kratos/kratos/pkg/net/netutil/breaker" - "github.com/go-kratos/kratos/pkg/net/rpc/warden" - pb "github.com/go-kratos/kratos/pkg/net/rpc/warden/internal/proto/testproto" - xtime "github.com/go-kratos/kratos/pkg/time" -) - -var ( - ccf = &warden.ClientConfig{ - Dial: xtime.Duration(time.Second * 10), - Timeout: xtime.Duration(time.Second * 10), - Breaker: &breaker.Config{ - Window: xtime.Duration(3 * time.Second), - Bucket: 10, - Request: 20, - K: 1.5, - }, - } - cli pb.GreeterClient - wg sync.WaitGroup - reqSize int - concurrency int - request int - all int64 -) - -func init() { - flag.IntVar(&reqSize, "s", 10, "request size") - flag.IntVar(&concurrency, "c", 10, "concurrency") - flag.IntVar(&request, "r", 1000, "request per routine") -} - -func main() { - flag.Parse() - name := randSeq(reqSize) - cli = newClient() - for i := 0; i < concurrency; i++ { - wg.Add(1) - go sayHello(&pb.HelloRequest{Name: name}) - } - wg.Wait() - fmt.Printf("per request cost %v\n", all/int64(request*concurrency)) -} - -func sayHello(in *pb.HelloRequest) { - defer wg.Done() - now := time.Now() - for i := 0; i < request; i++ { - cli.SayHello(context.TODO(), in) - } - delta := time.Since(now) - atomic.AddInt64(&all, int64(delta/time.Millisecond)) -} - -var letters = []rune("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ") - -func randSeq(n int) string { - b := make([]rune, n) - for i := range b { - b[i] = letters[rand.Intn(len(letters))] - } - return string(b) -} - -func newClient() (cli pb.GreeterClient) { - client := warden.NewClient(ccf) - conn, err := client.Dial(context.TODO(), "127.0.0.1:9999") - if err != nil { - return - } - cli = pb.NewGreeterClient(conn) - return -} diff --git a/pkg/net/rpc/warden/internal/benchmark/helloworld/server/greeter_server.go b/pkg/net/rpc/warden/internal/benchmark/helloworld/server/greeter_server.go deleted file mode 100644 index 7e621deb6..000000000 --- a/pkg/net/rpc/warden/internal/benchmark/helloworld/server/greeter_server.go +++ /dev/null @@ -1,49 +0,0 @@ -package main - -import ( - "context" - "net/http" - "time" - - "github.com/go-kratos/kratos/pkg/net/rpc/warden" - pb "github.com/go-kratos/kratos/pkg/net/rpc/warden/internal/proto/testproto" - xtime "github.com/go-kratos/kratos/pkg/time" - - "github.com/prometheus/client_golang/prometheus/promhttp" -) - -var ( - config = &warden.ServerConfig{Timeout: xtime.Duration(time.Second)} -) - -func main() { - newServer() -} - -type hello struct { -} - -func (s *hello) SayHello(c context.Context, in *pb.HelloRequest) (out *pb.HelloReply, err error) { - out = new(pb.HelloReply) - out.Message = in.Name - return -} - -func (s *hello) StreamHello(ss pb.Greeter_StreamHelloServer) error { - return nil -} -func newServer() { - server := warden.NewServer(config) - pb.RegisterGreeterServer(server.Server(), &hello{}) - go func() { - http.HandleFunc("/metrics", func(w http.ResponseWriter, r *http.Request) { - h := promhttp.Handler() - h.ServeHTTP(w, r) - }) - http.ListenAndServe("0.0.0.0:9998", nil) - }() - err := server.Run(":9999") - if err != nil { - return - } -} diff --git a/pkg/net/rpc/warden/internal/encoding/json/json.go b/pkg/net/rpc/warden/internal/encoding/json/json.go deleted file mode 100644 index dd1f320d5..000000000 --- a/pkg/net/rpc/warden/internal/encoding/json/json.go +++ /dev/null @@ -1,53 +0,0 @@ -package codec - -import ( - "bytes" - "encoding/json" - - "github.com/gogo/protobuf/jsonpb" - "github.com/gogo/protobuf/proto" - "google.golang.org/grpc/encoding" -) - -//Reference https://jbrandhorst.com/post/grpc-json/ -func init() { - encoding.RegisterCodec(JSON{ - Marshaler: jsonpb.Marshaler{ - EmitDefaults: true, - OrigName: true, - }, - }) -} - -// JSON is impl of encoding.Codec -type JSON struct { - jsonpb.Marshaler - jsonpb.Unmarshaler -} - -// Name is name of JSON -func (j JSON) Name() string { - return "json" -} - -// Marshal is json marshal -func (j JSON) Marshal(v interface{}) (out []byte, err error) { - if pm, ok := v.(proto.Message); ok { - b := new(bytes.Buffer) - err := j.Marshaler.Marshal(b, pm) - if err != nil { - return nil, err - } - return b.Bytes(), nil - } - return json.Marshal(v) -} - -// Unmarshal is json unmarshal -func (j JSON) Unmarshal(data []byte, v interface{}) (err error) { - if pm, ok := v.(proto.Message); ok { - b := bytes.NewBuffer(data) - return j.Unmarshaler.Unmarshal(b, pm) - } - return json.Unmarshal(data, v) -} diff --git a/pkg/net/rpc/warden/internal/examples/client/client.go b/pkg/net/rpc/warden/internal/examples/client/client.go deleted file mode 100644 index 54bd46b37..000000000 --- a/pkg/net/rpc/warden/internal/examples/client/client.go +++ /dev/null @@ -1,31 +0,0 @@ -package main - -import ( - "context" - "flag" - "fmt" - - "github.com/go-kratos/kratos/pkg/log" - "github.com/go-kratos/kratos/pkg/net/rpc/warden" - pb "github.com/go-kratos/kratos/pkg/net/rpc/warden/internal/proto/testproto" -) - -// usage: ./client -grpc.target=test.service=127.0.0.1:9000 -func main() { - log.Init(&log.Config{Stdout: true}) - flag.Parse() - conn, err := warden.NewClient(nil).Dial(context.Background(), "direct://default/127.0.0.1:9000") - if err != nil { - panic(err) - } - cli := pb.NewGreeterClient(conn) - normalCall(cli) -} - -func normalCall(cli pb.GreeterClient) { - reply, err := cli.SayHello(context.Background(), &pb.HelloRequest{Name: "tom", Age: 23}) - if err != nil { - panic(err) - } - fmt.Println("get reply:", *reply) -} diff --git a/pkg/net/rpc/warden/internal/examples/grpcDebug/client.go b/pkg/net/rpc/warden/internal/examples/grpcDebug/client.go deleted file mode 100644 index d2f1f232c..000000000 --- a/pkg/net/rpc/warden/internal/examples/grpcDebug/client.go +++ /dev/null @@ -1,191 +0,0 @@ -package main - -import ( - "context" - "encoding/json" - "flag" - "fmt" - "io/ioutil" - "math/rand" - "net/http" - "os" - "strings" - - "github.com/gogo/protobuf/jsonpb" - "google.golang.org/grpc" - "google.golang.org/grpc/credentials" - "google.golang.org/grpc/encoding" -) - -// Reply for test -type Reply struct { - res []byte -} - -type Discovery struct { - HttpClient *http.Client - Nodes []string -} - -var ( - data string - file string - method string - addr string - tlsCert string - tlsServerName string - appID string - env string -) - -//Reference https://jbrandhorst.com/post/grpc-json/ -func init() { - encoding.RegisterCodec(JSON{ - Marshaler: jsonpb.Marshaler{ - EmitDefaults: true, - OrigName: true, - }, - }) - flag.StringVar(&data, "data", `{"name":"longxia","age":19}`, `{"name":"longxia","age":19}`) - flag.StringVar(&file, "file", ``, `./data.json`) - flag.StringVar(&method, "method", "/testproto.Greeter/SayHello", `/testproto.Greeter/SayHello`) - flag.StringVar(&addr, "addr", "127.0.0.1:8080", `127.0.0.1:8080`) - flag.StringVar(&tlsCert, "cert", "", `./cert.pem`) - flag.StringVar(&tlsServerName, "server_name", "", `hello_server`) - flag.StringVar(&appID, "appid", "", `appid`) - flag.StringVar(&env, "env", "", `env`) -} - -// 该example因为使用的是json传输格式所以只能用于调试或测试,用于线上会导致性能下降 -// 使用方法: -// ./grpcDebug -data='{"name":"xia","age":19}' -addr=127.0.0.1:8080 -method=/testproto.Greeter/SayHello -// ./grpcDebug -file=data.json -addr=127.0.0.1:8080 -method=/testproto.Greeter/SayHello -// DEPLOY_ENV=uat ./grpcDebug -appid=main.community.reply-service -method=/reply.service.v1.Reply/ReplyInfoCache -data='{"rp_id"=1493769244}' -func main() { - flag.Parse() - opts := []grpc.DialOption{ - grpc.WithInsecure(), - grpc.WithDefaultCallOptions(grpc.CallContentSubtype(JSON{}.Name())), - } - if tlsCert != "" { - creds, err := credentials.NewClientTLSFromFile(tlsCert, tlsServerName) - if err != nil { - panic(err) - } - opts = append(opts, grpc.WithTransportCredentials(creds)) - } - if file != "" { - content, err := ioutil.ReadFile(file) - if err != nil { - fmt.Printf("ioutil.ReadFile(%s) error(%v)\n", file, err) - os.Exit(1) - } - if len(content) > 0 { - data = string(content) - } - } - if appID != "" { - addr = ipFromDiscovery(appID, env) - } - conn, err := grpc.Dial(addr, opts...) - if err != nil { - panic(err) - } - var reply Reply - err = grpc.Invoke(context.Background(), method, []byte(data), &reply, conn) - if err != nil { - panic(err) - } - fmt.Println(string(reply.res)) -} - -func ipFromDiscovery(appID, env string) string { - d := &Discovery{ - Nodes: []string{"discovery.bilibili.co", "api.bilibili.co"}, - HttpClient: http.DefaultClient, - } - deployEnv := os.Getenv("DEPLOY_ENV") - if deployEnv != "" { - env = deployEnv - } - return d.addr(appID, env, d.nodes()) -} - -func (d *Discovery) nodes() (addrs []string) { - res := new(struct { - Code int `json:"code"` - Data []struct { - Addr string `json:"addr"` - } `json:"data"` - }) - resp, err := d.HttpClient.Get(fmt.Sprintf("http://%s/discovery/nodes", d.Nodes[rand.Intn(len(d.Nodes))])) - if err != nil { - panic(err) - } - defer resp.Body.Close() - if err = json.NewDecoder(resp.Body).Decode(&res); err != nil { - panic(err) - } - for _, data := range res.Data { - addrs = append(addrs, data.Addr) - } - return -} - -func (d *Discovery) addr(appID, env string, nodes []string) (ip string) { - res := new(struct { - Code int `json:"code"` - Message string `json:"message"` - Data map[string]*struct { - ZoneInstances map[string][]*struct { - AppID string `json:"appid"` - Addrs []string `json:"addrs"` - } `json:"zone_instances"` - } `json:"data"` - }) - host, _ := os.Hostname() - resp, err := d.HttpClient.Get(fmt.Sprintf("http://%s/discovery/polls?appid=%s&env=%s&hostname=%s", nodes[rand.Intn(len(nodes))], appID, env, host)) - if err != nil { - panic(err) - } - defer resp.Body.Close() - if err = json.NewDecoder(resp.Body).Decode(&res); err != nil { - panic(err) - } - for _, data := range res.Data { - for _, zoneInstance := range data.ZoneInstances { - for _, instance := range zoneInstance { - if instance.AppID == appID { - for _, addr := range instance.Addrs { - if strings.Contains(addr, "grpc://") { - return strings.Replace(addr, "grpc://", "", -1) - } - } - } - } - } - } - return -} - -// JSON is impl of encoding.Codec -type JSON struct { - jsonpb.Marshaler - jsonpb.Unmarshaler -} - -// Name is name of JSON -func (j JSON) Name() string { - return "json" -} - -// Marshal is json marshal -func (j JSON) Marshal(v interface{}) (out []byte, err error) { - return v.([]byte), nil -} - -// Unmarshal is json unmarshal -func (j JSON) Unmarshal(data []byte, v interface{}) (err error) { - v.(*Reply).res = data - return nil -} diff --git a/pkg/net/rpc/warden/internal/examples/grpcDebug/data.json b/pkg/net/rpc/warden/internal/examples/grpcDebug/data.json deleted file mode 100644 index 3e65f07e1..000000000 --- a/pkg/net/rpc/warden/internal/examples/grpcDebug/data.json +++ /dev/null @@ -1 +0,0 @@ -{"name":"xia","age":19} \ No newline at end of file diff --git a/pkg/net/rpc/warden/internal/examples/server/main.go b/pkg/net/rpc/warden/internal/examples/server/main.go deleted file mode 100644 index 3c37abdb8..000000000 --- a/pkg/net/rpc/warden/internal/examples/server/main.go +++ /dev/null @@ -1,104 +0,0 @@ -package main - -import ( - "context" - "fmt" - "io" - "os" - "os/signal" - "syscall" - "time" - - "github.com/go-kratos/kratos/pkg/ecode" - "github.com/go-kratos/kratos/pkg/log" - "github.com/go-kratos/kratos/pkg/net/rpc/warden" - pb "github.com/go-kratos/kratos/pkg/net/rpc/warden/internal/proto/testproto" - xtime "github.com/go-kratos/kratos/pkg/time" - - "google.golang.org/grpc" -) - -type helloServer struct { - addr string -} - -func (s *helloServer) SayHello(ctx context.Context, in *pb.HelloRequest) (*pb.HelloReply, error) { - if in.Name == "err_detail_test" { - err, _ := ecode.Error(ecode.AccessDenied, "AccessDenied").WithDetails(&pb.HelloReply{Success: true, Message: "this is test detail"}) - return nil, err - } - return &pb.HelloReply{Message: fmt.Sprintf("hello %s from %s", in.Name, s.addr)}, nil -} - -func (s *helloServer) StreamHello(ss pb.Greeter_StreamHelloServer) error { - for i := 0; i < 3; i++ { - in, err := ss.Recv() - if err == io.EOF { - return nil - } - if err != nil { - return err - } - ret := &pb.HelloReply{Message: "Hello " + in.Name, Success: true} - err = ss.Send(ret) - if err != nil { - return err - } - } - return nil -} - -func runServer(addr string) *warden.Server { - server := warden.NewServer(&warden.ServerConfig{ - //服务端每个请求的默认超时时间 - Timeout: xtime.Duration(time.Second), - }) - server.Use(middleware()) - pb.RegisterGreeterServer(server.Server(), &helloServer{addr: addr}) - go func() { - err := server.Run(addr) - if err != nil { - panic("run server failed!" + err.Error()) - } - }() - return server -} - -func main() { - log.Init(&log.Config{Stdout: true}) - server := runServer("0.0.0.0:8081") - signalHandler(server) -} - -//类似于中间件 -func middleware() grpc.UnaryServerInterceptor { - return func(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (resp interface{}, err error) { - //记录调用方法 - log.Info("method:%s", info.FullMethod) - //call chain - resp, err = handler(ctx, req) - return - } -} - -func signalHandler(s *warden.Server) { - var ( - ch = make(chan os.Signal, 1) - ) - signal.Notify(ch, syscall.SIGHUP, syscall.SIGQUIT, syscall.SIGTERM, syscall.SIGINT) - for { - si := <-ch - switch si { - case syscall.SIGQUIT, syscall.SIGTERM, syscall.SIGINT: - log.Info("get a signal %s, stop the consume process", si.String()) - ctx, cancel := context.WithTimeout(context.Background(), time.Second*3) - defer cancel() - //gracefully shutdown with timeout - s.Shutdown(ctx) - return - case syscall.SIGHUP: - default: - return - } - } -} diff --git a/pkg/net/rpc/warden/internal/metadata/metadata.go b/pkg/net/rpc/warden/internal/metadata/metadata.go deleted file mode 100644 index e1fe5ba78..000000000 --- a/pkg/net/rpc/warden/internal/metadata/metadata.go +++ /dev/null @@ -1,11 +0,0 @@ -package metadata - -const ( - CPUUsage = "cpu_usage" -) - -// MD is context metadata for balancer and resolver -type MD struct { - Weight uint64 - Color string -} diff --git a/pkg/net/rpc/warden/internal/proto/testproto/hello.pb.go b/pkg/net/rpc/warden/internal/proto/testproto/hello.pb.go deleted file mode 100644 index 9c9679b9e..000000000 --- a/pkg/net/rpc/warden/internal/proto/testproto/hello.pb.go +++ /dev/null @@ -1,642 +0,0 @@ -// Code generated by protoc-gen-gogo. DO NOT EDIT. -// source: hello.proto - -/* - Package testproto is a generated protocol buffer package. - - It is generated from these files: - hello.proto - - It has these top-level messages: - HelloRequest - HelloReply -*/ -package testproto - -import proto "github.com/golang/protobuf/proto" -import fmt "fmt" -import math "math" -import _ "github.com/gogo/protobuf/gogoproto" - -import context "golang.org/x/net/context" -import grpc "google.golang.org/grpc" - -import io "io" - -// Reference imports to suppress errors if they are not otherwise used. -var _ = proto.Marshal -var _ = fmt.Errorf -var _ = math.Inf - -// This is a compile-time assertion to ensure that this generated file -// is compatible with the proto package it is being compiled against. -// A compilation error at this line likely means your copy of the -// proto package needs to be updated. -const _ = proto.ProtoPackageIsVersion2 // please upgrade the proto package - -// The request message containing the user's name. -type HelloRequest struct { - Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name" validate:"required"` - Age int32 `protobuf:"varint,2,opt,name=age,proto3" json:"age" validate:"min=0"` -} - -func (m *HelloRequest) Reset() { *m = HelloRequest{} } -func (m *HelloRequest) String() string { return proto.CompactTextString(m) } -func (*HelloRequest) ProtoMessage() {} -func (*HelloRequest) Descriptor() ([]byte, []int) { return fileDescriptorHello, []int{0} } - -// The response message containing the greetings -type HelloReply struct { - Message string `protobuf:"bytes,1,opt,name=message,proto3" json:"message,omitempty"` - Success bool `protobuf:"varint,2,opt,name=success,proto3" json:"success,omitempty"` -} - -func (m *HelloReply) Reset() { *m = HelloReply{} } -func (m *HelloReply) String() string { return proto.CompactTextString(m) } -func (*HelloReply) ProtoMessage() {} -func (*HelloReply) Descriptor() ([]byte, []int) { return fileDescriptorHello, []int{1} } - -func init() { - proto.RegisterType((*HelloRequest)(nil), "testproto.HelloRequest") - proto.RegisterType((*HelloReply)(nil), "testproto.HelloReply") -} - -// Reference imports to suppress errors if they are not otherwise used. -var _ context.Context -var _ grpc.ClientConn - -// This is a compile-time assertion to ensure that this generated file -// is compatible with the grpc package it is being compiled against. -const _ = grpc.SupportPackageIsVersion4 - -// Client API for Greeter service - -type GreeterClient interface { - // Sends a greeting - SayHello(ctx context.Context, in *HelloRequest, opts ...grpc.CallOption) (*HelloReply, error) - // A bidirectional streaming RPC call recvice HelloRequest return HelloReply - StreamHello(ctx context.Context, opts ...grpc.CallOption) (Greeter_StreamHelloClient, error) -} - -type greeterClient struct { - cc *grpc.ClientConn -} - -func NewGreeterClient(cc *grpc.ClientConn) GreeterClient { - return &greeterClient{cc} -} - -func (c *greeterClient) SayHello(ctx context.Context, in *HelloRequest, opts ...grpc.CallOption) (*HelloReply, error) { - out := new(HelloReply) - err := grpc.Invoke(ctx, "/testproto.Greeter/SayHello", in, out, c.cc, opts...) - if err != nil { - return nil, err - } - return out, nil -} - -func (c *greeterClient) StreamHello(ctx context.Context, opts ...grpc.CallOption) (Greeter_StreamHelloClient, error) { - stream, err := grpc.NewClientStream(ctx, &_Greeter_serviceDesc.Streams[0], c.cc, "/testproto.Greeter/StreamHello", opts...) - if err != nil { - return nil, err - } - x := &greeterStreamHelloClient{stream} - return x, nil -} - -type Greeter_StreamHelloClient interface { - Send(*HelloRequest) error - Recv() (*HelloReply, error) - grpc.ClientStream -} - -type greeterStreamHelloClient struct { - grpc.ClientStream -} - -func (x *greeterStreamHelloClient) Send(m *HelloRequest) error { - return x.ClientStream.SendMsg(m) -} - -func (x *greeterStreamHelloClient) Recv() (*HelloReply, error) { - m := new(HelloReply) - if err := x.ClientStream.RecvMsg(m); err != nil { - return nil, err - } - return m, nil -} - -// Server API for Greeter service - -type GreeterServer interface { - // Sends a greeting - SayHello(context.Context, *HelloRequest) (*HelloReply, error) - // A bidirectional streaming RPC call recvice HelloRequest return HelloReply - StreamHello(Greeter_StreamHelloServer) error -} - -func RegisterGreeterServer(s *grpc.Server, srv GreeterServer) { - s.RegisterService(&_Greeter_serviceDesc, srv) -} - -func _Greeter_SayHello_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { - in := new(HelloRequest) - if err := dec(in); err != nil { - return nil, err - } - if interceptor == nil { - return srv.(GreeterServer).SayHello(ctx, in) - } - info := &grpc.UnaryServerInfo{ - Server: srv, - FullMethod: "/testproto.Greeter/SayHello", - } - handler := func(ctx context.Context, req interface{}) (interface{}, error) { - return srv.(GreeterServer).SayHello(ctx, req.(*HelloRequest)) - } - return interceptor(ctx, in, info, handler) -} - -func _Greeter_StreamHello_Handler(srv interface{}, stream grpc.ServerStream) error { - return srv.(GreeterServer).StreamHello(&greeterStreamHelloServer{stream}) -} - -type Greeter_StreamHelloServer interface { - Send(*HelloReply) error - Recv() (*HelloRequest, error) - grpc.ServerStream -} - -type greeterStreamHelloServer struct { - grpc.ServerStream -} - -func (x *greeterStreamHelloServer) Send(m *HelloReply) error { - return x.ServerStream.SendMsg(m) -} - -func (x *greeterStreamHelloServer) Recv() (*HelloRequest, error) { - m := new(HelloRequest) - if err := x.ServerStream.RecvMsg(m); err != nil { - return nil, err - } - return m, nil -} - -var _Greeter_serviceDesc = grpc.ServiceDesc{ - ServiceName: "testproto.Greeter", - HandlerType: (*GreeterServer)(nil), - Methods: []grpc.MethodDesc{ - { - MethodName: "SayHello", - Handler: _Greeter_SayHello_Handler, - }, - }, - Streams: []grpc.StreamDesc{ - { - StreamName: "StreamHello", - Handler: _Greeter_StreamHello_Handler, - ServerStreams: true, - ClientStreams: true, - }, - }, - Metadata: "hello.proto", -} - -func (m *HelloRequest) Marshal() (dAtA []byte, err error) { - size := m.Size() - dAtA = make([]byte, size) - n, err := m.MarshalTo(dAtA) - if err != nil { - return nil, err - } - return dAtA[:n], nil -} - -func (m *HelloRequest) MarshalTo(dAtA []byte) (int, error) { - var i int - _ = i - var l int - _ = l - if len(m.Name) > 0 { - dAtA[i] = 0xa - i++ - i = encodeVarintHello(dAtA, i, uint64(len(m.Name))) - i += copy(dAtA[i:], m.Name) - } - if m.Age != 0 { - dAtA[i] = 0x10 - i++ - i = encodeVarintHello(dAtA, i, uint64(m.Age)) - } - return i, nil -} - -func (m *HelloReply) Marshal() (dAtA []byte, err error) { - size := m.Size() - dAtA = make([]byte, size) - n, err := m.MarshalTo(dAtA) - if err != nil { - return nil, err - } - return dAtA[:n], nil -} - -func (m *HelloReply) MarshalTo(dAtA []byte) (int, error) { - var i int - _ = i - var l int - _ = l - if len(m.Message) > 0 { - dAtA[i] = 0xa - i++ - i = encodeVarintHello(dAtA, i, uint64(len(m.Message))) - i += copy(dAtA[i:], m.Message) - } - if m.Success { - dAtA[i] = 0x10 - i++ - if m.Success { - dAtA[i] = 1 - } else { - dAtA[i] = 0 - } - i++ - } - return i, nil -} - -func encodeVarintHello(dAtA []byte, offset int, v uint64) int { - for v >= 1<<7 { - dAtA[offset] = uint8(v&0x7f | 0x80) - v >>= 7 - offset++ - } - dAtA[offset] = uint8(v) - return offset + 1 -} -func (m *HelloRequest) Size() (n int) { - var l int - _ = l - l = len(m.Name) - if l > 0 { - n += 1 + l + sovHello(uint64(l)) - } - if m.Age != 0 { - n += 1 + sovHello(uint64(m.Age)) - } - return n -} - -func (m *HelloReply) Size() (n int) { - var l int - _ = l - l = len(m.Message) - if l > 0 { - n += 1 + l + sovHello(uint64(l)) - } - if m.Success { - n += 2 - } - return n -} - -func sovHello(x uint64) (n int) { - for { - n++ - x >>= 7 - if x == 0 { - break - } - } - return n -} -func sozHello(x uint64) (n int) { - return sovHello(uint64((x << 1) ^ uint64((int64(x) >> 63)))) -} -func (m *HelloRequest) Unmarshal(dAtA []byte) error { - l := len(dAtA) - iNdEx := 0 - for iNdEx < l { - preIndex := iNdEx - var wire uint64 - for shift := uint(0); ; shift += 7 { - if shift >= 64 { - return ErrIntOverflowHello - } - if iNdEx >= l { - return io.ErrUnexpectedEOF - } - b := dAtA[iNdEx] - iNdEx++ - wire |= (uint64(b) & 0x7F) << shift - if b < 0x80 { - break - } - } - fieldNum := int32(wire >> 3) - wireType := int(wire & 0x7) - if wireType == 4 { - return fmt.Errorf("proto: HelloRequest: wiretype end group for non-group") - } - if fieldNum <= 0 { - return fmt.Errorf("proto: HelloRequest: illegal tag %d (wire type %d)", fieldNum, wire) - } - switch fieldNum { - case 1: - if wireType != 2 { - return fmt.Errorf("proto: wrong wireType = %d for field Name", wireType) - } - var stringLen uint64 - for shift := uint(0); ; shift += 7 { - if shift >= 64 { - return ErrIntOverflowHello - } - if iNdEx >= l { - return io.ErrUnexpectedEOF - } - b := dAtA[iNdEx] - iNdEx++ - stringLen |= (uint64(b) & 0x7F) << shift - if b < 0x80 { - break - } - } - intStringLen := int(stringLen) - if intStringLen < 0 { - return ErrInvalidLengthHello - } - postIndex := iNdEx + intStringLen - if postIndex > l { - return io.ErrUnexpectedEOF - } - m.Name = string(dAtA[iNdEx:postIndex]) - iNdEx = postIndex - case 2: - if wireType != 0 { - return fmt.Errorf("proto: wrong wireType = %d for field Age", wireType) - } - m.Age = 0 - for shift := uint(0); ; shift += 7 { - if shift >= 64 { - return ErrIntOverflowHello - } - if iNdEx >= l { - return io.ErrUnexpectedEOF - } - b := dAtA[iNdEx] - iNdEx++ - m.Age |= (int32(b) & 0x7F) << shift - if b < 0x80 { - break - } - } - default: - iNdEx = preIndex - skippy, err := skipHello(dAtA[iNdEx:]) - if err != nil { - return err - } - if skippy < 0 { - return ErrInvalidLengthHello - } - if (iNdEx + skippy) > l { - return io.ErrUnexpectedEOF - } - iNdEx += skippy - } - } - - if iNdEx > l { - return io.ErrUnexpectedEOF - } - return nil -} -func (m *HelloReply) Unmarshal(dAtA []byte) error { - l := len(dAtA) - iNdEx := 0 - for iNdEx < l { - preIndex := iNdEx - var wire uint64 - for shift := uint(0); ; shift += 7 { - if shift >= 64 { - return ErrIntOverflowHello - } - if iNdEx >= l { - return io.ErrUnexpectedEOF - } - b := dAtA[iNdEx] - iNdEx++ - wire |= (uint64(b) & 0x7F) << shift - if b < 0x80 { - break - } - } - fieldNum := int32(wire >> 3) - wireType := int(wire & 0x7) - if wireType == 4 { - return fmt.Errorf("proto: HelloReply: wiretype end group for non-group") - } - if fieldNum <= 0 { - return fmt.Errorf("proto: HelloReply: illegal tag %d (wire type %d)", fieldNum, wire) - } - switch fieldNum { - case 1: - if wireType != 2 { - return fmt.Errorf("proto: wrong wireType = %d for field Message", wireType) - } - var stringLen uint64 - for shift := uint(0); ; shift += 7 { - if shift >= 64 { - return ErrIntOverflowHello - } - if iNdEx >= l { - return io.ErrUnexpectedEOF - } - b := dAtA[iNdEx] - iNdEx++ - stringLen |= (uint64(b) & 0x7F) << shift - if b < 0x80 { - break - } - } - intStringLen := int(stringLen) - if intStringLen < 0 { - return ErrInvalidLengthHello - } - postIndex := iNdEx + intStringLen - if postIndex > l { - return io.ErrUnexpectedEOF - } - m.Message = string(dAtA[iNdEx:postIndex]) - iNdEx = postIndex - case 2: - if wireType != 0 { - return fmt.Errorf("proto: wrong wireType = %d for field Success", wireType) - } - var v int - for shift := uint(0); ; shift += 7 { - if shift >= 64 { - return ErrIntOverflowHello - } - if iNdEx >= l { - return io.ErrUnexpectedEOF - } - b := dAtA[iNdEx] - iNdEx++ - v |= (int(b) & 0x7F) << shift - if b < 0x80 { - break - } - } - m.Success = bool(v != 0) - default: - iNdEx = preIndex - skippy, err := skipHello(dAtA[iNdEx:]) - if err != nil { - return err - } - if skippy < 0 { - return ErrInvalidLengthHello - } - if (iNdEx + skippy) > l { - return io.ErrUnexpectedEOF - } - iNdEx += skippy - } - } - - if iNdEx > l { - return io.ErrUnexpectedEOF - } - return nil -} -func skipHello(dAtA []byte) (n int, err error) { - l := len(dAtA) - iNdEx := 0 - for iNdEx < l { - var wire uint64 - for shift := uint(0); ; shift += 7 { - if shift >= 64 { - return 0, ErrIntOverflowHello - } - if iNdEx >= l { - return 0, io.ErrUnexpectedEOF - } - b := dAtA[iNdEx] - iNdEx++ - wire |= (uint64(b) & 0x7F) << shift - if b < 0x80 { - break - } - } - wireType := int(wire & 0x7) - switch wireType { - case 0: - for shift := uint(0); ; shift += 7 { - if shift >= 64 { - return 0, ErrIntOverflowHello - } - if iNdEx >= l { - return 0, io.ErrUnexpectedEOF - } - iNdEx++ - if dAtA[iNdEx-1] < 0x80 { - break - } - } - return iNdEx, nil - case 1: - iNdEx += 8 - return iNdEx, nil - case 2: - var length int - for shift := uint(0); ; shift += 7 { - if shift >= 64 { - return 0, ErrIntOverflowHello - } - if iNdEx >= l { - return 0, io.ErrUnexpectedEOF - } - b := dAtA[iNdEx] - iNdEx++ - length |= (int(b) & 0x7F) << shift - if b < 0x80 { - break - } - } - iNdEx += length - if length < 0 { - return 0, ErrInvalidLengthHello - } - return iNdEx, nil - case 3: - for { - var innerWire uint64 - var start int = iNdEx - for shift := uint(0); ; shift += 7 { - if shift >= 64 { - return 0, ErrIntOverflowHello - } - if iNdEx >= l { - return 0, io.ErrUnexpectedEOF - } - b := dAtA[iNdEx] - iNdEx++ - innerWire |= (uint64(b) & 0x7F) << shift - if b < 0x80 { - break - } - } - innerWireType := int(innerWire & 0x7) - if innerWireType == 4 { - break - } - next, err := skipHello(dAtA[start:]) - if err != nil { - return 0, err - } - iNdEx = start + next - } - return iNdEx, nil - case 4: - return iNdEx, nil - case 5: - iNdEx += 4 - return iNdEx, nil - default: - return 0, fmt.Errorf("proto: illegal wireType %d", wireType) - } - } - panic("unreachable") -} - -var ( - ErrInvalidLengthHello = fmt.Errorf("proto: negative length found during unmarshaling") - ErrIntOverflowHello = fmt.Errorf("proto: integer overflow") -) - -func init() { proto.RegisterFile("hello.proto", fileDescriptorHello) } - -var fileDescriptorHello = []byte{ - // 296 bytes of a gzipped FileDescriptorProto - 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0x9c, 0x90, 0x3f, 0x4e, 0xc3, 0x30, - 0x14, 0xc6, 0x63, 0xfe, 0xb5, 0x75, 0x19, 0x90, 0x11, 0x22, 0x2a, 0x92, 0x53, 0x79, 0xca, 0xd2, - 0xb4, 0xa2, 0x1b, 0x02, 0x09, 0x85, 0x01, 0xe6, 0xf4, 0x04, 0x4e, 0xfa, 0x48, 0x23, 0x25, 0x75, - 0x6a, 0x3b, 0x48, 0xb9, 0x03, 0x07, 0xe0, 0x48, 0x1d, 0x7b, 0x82, 0x88, 0x86, 0xad, 0x63, 0x4f, - 0x80, 0x62, 0x28, 0x20, 0xb1, 0x75, 0x7b, 0x3f, 0x7f, 0xfa, 0x7e, 0x4f, 0x7e, 0xb8, 0x3b, 0x83, - 0x34, 0x15, 0x5e, 0x2e, 0x85, 0x16, 0xa4, 0xa3, 0x41, 0x69, 0x33, 0xf6, 0x06, 0x71, 0xa2, 0x67, - 0x45, 0xe8, 0x45, 0x22, 0x1b, 0xc6, 0x22, 0x16, 0x43, 0xf3, 0x1c, 0x16, 0xcf, 0x86, 0x0c, 0x98, - 0xe9, 0xab, 0xc9, 0x24, 0x3e, 0x7d, 0x6a, 0x44, 0x01, 0x2c, 0x0a, 0x50, 0x9a, 0x8c, 0xf1, 0xd1, - 0x9c, 0x67, 0x60, 0xa3, 0x3e, 0x72, 0x3b, 0xbe, 0xb3, 0xa9, 0x1c, 0xc3, 0xdb, 0xca, 0x39, 0x7f, - 0xe1, 0x69, 0x32, 0xe5, 0x1a, 0x6e, 0x98, 0x84, 0x45, 0x91, 0x48, 0x98, 0xb2, 0xc0, 0x84, 0x64, - 0x80, 0x0f, 0x79, 0x0c, 0xf6, 0x41, 0x1f, 0xb9, 0xc7, 0xfe, 0xd5, 0xa6, 0x72, 0x1a, 0xdc, 0x56, - 0xce, 0xd9, 0x6f, 0x25, 0x4b, 0xe6, 0x77, 0x23, 0x16, 0x34, 0x01, 0xbb, 0xc7, 0xf8, 0x7b, 0x67, - 0x9e, 0x96, 0xc4, 0xc6, 0xad, 0x0c, 0x94, 0x6a, 0x04, 0x66, 0x69, 0xb0, 0xc3, 0x26, 0x51, 0x45, - 0x14, 0x81, 0x52, 0x46, 0xdd, 0x0e, 0x76, 0x78, 0xfd, 0x8a, 0x70, 0xeb, 0x51, 0x02, 0x68, 0x90, - 0xe4, 0x16, 0xb7, 0x27, 0xbc, 0x34, 0x42, 0x72, 0xe9, 0xfd, 0x1c, 0xc2, 0xfb, 0xfb, 0xad, 0xde, - 0xc5, 0xff, 0x20, 0x4f, 0x4b, 0x66, 0x91, 0x07, 0xdc, 0x9d, 0x68, 0x09, 0x3c, 0xdb, 0x53, 0xe0, - 0xa2, 0x11, 0xf2, 0xed, 0xe5, 0x9a, 0x5a, 0xab, 0x35, 0xb5, 0x96, 0x35, 0x45, 0xab, 0x9a, 0xa2, - 0xf7, 0x9a, 0xa2, 0xb7, 0x0f, 0x6a, 0x85, 0x27, 0xa6, 0x31, 0xfe, 0x0c, 0x00, 0x00, 0xff, 0xff, - 0x13, 0x57, 0x88, 0x03, 0xae, 0x01, 0x00, 0x00, -} diff --git a/pkg/net/rpc/warden/internal/proto/testproto/hello.proto b/pkg/net/rpc/warden/internal/proto/testproto/hello.proto deleted file mode 100644 index 4fd6d24c3..000000000 --- a/pkg/net/rpc/warden/internal/proto/testproto/hello.proto +++ /dev/null @@ -1,33 +0,0 @@ -syntax = "proto3"; - -package testproto; - -import "github.com/gogo/protobuf/gogoproto/gogo.proto"; - -option (gogoproto.goproto_enum_prefix_all) = false; -option (gogoproto.goproto_getters_all) = false; -option (gogoproto.unmarshaler_all) = true; -option (gogoproto.marshaler_all) = true; -option (gogoproto.sizer_all) = true; -option (gogoproto.goproto_registration) = true; - -// The greeting service definition. -service Greeter { - // Sends a greeting - rpc SayHello (HelloRequest) returns (HelloReply) {} - - // A bidirectional streaming RPC call recvice HelloRequest return HelloReply - rpc StreamHello(stream HelloRequest) returns (stream HelloReply) {} -} - -// The request message containing the user's name. -message HelloRequest { - string name = 1 [(gogoproto.jsontag) = "name", (gogoproto.moretags) = "validate:\"required\""]; - int32 age = 2 [(gogoproto.jsontag) = "age", (gogoproto.moretags) = "validate:\"min=0\""]; -} - -// The response message containing the greetings -message HelloReply { - string message = 1; - bool success = 2; -} diff --git a/pkg/net/rpc/warden/internal/status/status.go b/pkg/net/rpc/warden/internal/status/status.go deleted file mode 100644 index 3e3b02b1e..000000000 --- a/pkg/net/rpc/warden/internal/status/status.go +++ /dev/null @@ -1,120 +0,0 @@ -package status - -import ( - "context" - "strconv" - - "github.com/go-kratos/kratos/pkg/ecode" - - "github.com/golang/protobuf/proto" - "github.com/pkg/errors" - "google.golang.org/grpc/codes" - "google.golang.org/grpc/status" -) - -// togRPCCode convert ecode.Codo to gRPC code -func togRPCCode(code ecode.Codes) codes.Code { - switch code.Code() { - case ecode.OK.Code(): - return codes.OK - case ecode.RequestErr.Code(): - return codes.InvalidArgument - case ecode.NothingFound.Code(): - return codes.NotFound - case ecode.Unauthorized.Code(): - return codes.Unauthenticated - case ecode.AccessDenied.Code(): - return codes.PermissionDenied - case ecode.LimitExceed.Code(): - return codes.ResourceExhausted - case ecode.MethodNotAllowed.Code(): - return codes.Unimplemented - case ecode.Deadline.Code(): - return codes.DeadlineExceeded - case ecode.ServiceUnavailable.Code(): - return codes.Unavailable - } - return codes.Unknown -} - -func toECode(gst *status.Status) ecode.Code { - gcode := gst.Code() - switch gcode { - case codes.OK: - return ecode.OK - case codes.InvalidArgument: - return ecode.RequestErr - case codes.NotFound: - return ecode.NothingFound - case codes.PermissionDenied: - return ecode.AccessDenied - case codes.Unauthenticated: - return ecode.Unauthorized - case codes.ResourceExhausted: - return ecode.LimitExceed - case codes.Unimplemented: - return ecode.MethodNotAllowed - case codes.DeadlineExceeded: - return ecode.Deadline - case codes.Unavailable: - return ecode.ServiceUnavailable - case codes.Unknown: - return ecode.String(gst.Message()) - } - return ecode.ServerErr -} - -// FromError convert error for service reply and try to convert it to grpc.Status. -func FromError(svrErr error) (gst *status.Status) { - var err error - svrErr = errors.Cause(svrErr) - if code, ok := svrErr.(ecode.Codes); ok { - // TODO: deal with err - if gst, err = gRPCStatusFromEcode(code); err == nil { - return - } - } - // for some special error convert context.Canceled to ecode.Canceled, - // context.DeadlineExceeded to ecode.DeadlineExceeded only for raw error - // if err be wrapped will not effect. - switch svrErr { - case context.Canceled: - gst, _ = gRPCStatusFromEcode(ecode.Canceled) - case context.DeadlineExceeded: - gst, _ = gRPCStatusFromEcode(ecode.Deadline) - default: - gst, _ = status.FromError(svrErr) - } - return -} - -func gRPCStatusFromEcode(code ecode.Codes) (*status.Status, error) { - var st *ecode.Status - switch v := code.(type) { - case *ecode.Status: - st = v - case ecode.Code: - st = ecode.FromCode(v) - default: - st = ecode.Error(ecode.Code(code.Code()), code.Message()) - for _, detail := range code.Details() { - if msg, ok := detail.(proto.Message); ok { - st.WithDetails(msg) - } - } - } - gst := status.New(codes.Unknown, strconv.Itoa(st.Code())) - return gst.WithDetails(st.Proto()) -} - -// ToEcode convert grpc.status to ecode.Codes -func ToEcode(gst *status.Status) ecode.Codes { - details := gst.Details() - for _, detail := range details { - // convert detail to status only use first detail - if pb, ok := detail.(proto.Message); ok { - return ecode.FromProto(pb) - } - } - return toECode(gst) -} diff --git a/pkg/net/rpc/warden/internal/status/status_test.go b/pkg/net/rpc/warden/internal/status/status_test.go deleted file mode 100644 index 5d996419d..000000000 --- a/pkg/net/rpc/warden/internal/status/status_test.go +++ /dev/null @@ -1,125 +0,0 @@ -package status - -import ( - "context" - "errors" - "fmt" - "testing" - "time" - - "github.com/golang/protobuf/ptypes/timestamp" - pkgerr "github.com/pkg/errors" - "github.com/stretchr/testify/assert" - "google.golang.org/grpc/codes" - "google.golang.org/grpc/status" - - "github.com/go-kratos/kratos/pkg/ecode" -) - -func TestCodeConvert(t *testing.T) { - var table = map[codes.Code]ecode.Code{ - codes.OK: ecode.OK, - // codes.Canceled - codes.Unknown: ecode.ServerErr, - codes.InvalidArgument: ecode.RequestErr, - codes.DeadlineExceeded: ecode.Deadline, - codes.NotFound: ecode.NothingFound, - // codes.AlreadyExists - codes.PermissionDenied: ecode.AccessDenied, - codes.ResourceExhausted: ecode.LimitExceed, - // codes.FailedPrecondition - // codes.Aborted - // codes.OutOfRange - codes.Unimplemented: ecode.MethodNotAllowed, - codes.Unavailable: ecode.ServiceUnavailable, - // codes.DataLoss - codes.Unauthenticated: ecode.Unauthorized, - } - for k, v := range table { - assert.Equal(t, toECode(status.New(k, "-500")), v) - } - for k, v := range table { - assert.Equal(t, togRPCCode(v), k, fmt.Sprintf("togRPC code error: %d -> %d", v, k)) - } -} - -func TestNoDetailsConvert(t *testing.T) { - gst := status.New(codes.Unknown, "-2233") - assert.Equal(t, toECode(gst).Code(), -2233) - - gst = status.New(codes.Internal, "") - assert.Equal(t, toECode(gst).Code(), -500) -} - -func TestFromError(t *testing.T) { - t.Run("input general error", func(t *testing.T) { - err := errors.New("general error") - gst := FromError(err) - - assert.Equal(t, codes.Unknown, gst.Code()) - assert.Contains(t, gst.Message(), "general") - }) - t.Run("input wrap error", func(t *testing.T) { - err := pkgerr.Wrap(ecode.RequestErr, "hh") - gst := FromError(err) - - assert.Equal(t, "-400", gst.Message()) - }) - t.Run("input ecode.Code", func(t *testing.T) { - err := ecode.RequestErr - gst := FromError(err) - - //assert.Equal(t, codes.InvalidArgument, gst.Code()) - // NOTE: set all grpc.status as Unknown when error is ecode.Codes for compatible - assert.Equal(t, codes.Unknown, gst.Code()) - // NOTE: gst.Message == str(ecode.Code) for compatible php leagcy code - assert.Equal(t, err.Message(), gst.Message()) - }) - t.Run("input raw Canceled", func(t *testing.T) { - gst := FromError(context.Canceled) - - assert.Equal(t, codes.Unknown, gst.Code()) - assert.Equal(t, "-498", gst.Message()) - }) - t.Run("input raw DeadlineExceeded", func(t *testing.T) { - gst := FromError(context.DeadlineExceeded) - - assert.Equal(t, codes.Unknown, gst.Code()) - assert.Equal(t, "-504", gst.Message()) - }) - t.Run("input ecode.Status", func(t *testing.T) { - m := ×tamp.Timestamp{Seconds: time.Now().Unix()} - err, _ := ecode.Error(ecode.Unauthorized, "unauthorized").WithDetails(m) - gst := FromError(err) - - //assert.Equal(t, codes.Unauthenticated, gst.Code()) - // NOTE: set all grpc.status as Unknown when error is ecode.Codes for compatible - assert.Equal(t, codes.Unknown, gst.Code()) - assert.Len(t, gst.Details(), 1) - details := gst.Details() - assert.IsType(t, err.Proto(), details[0]) - }) -} - -func TestToEcode(t *testing.T) { - t.Run("input general grpc.Status", func(t *testing.T) { - gst := status.New(codes.Unknown, "unknown") - ec := ToEcode(gst) - - assert.Equal(t, int(ecode.ServerErr), ec.Code()) - assert.Equal(t, "-500", ec.Message()) - assert.Len(t, ec.Details(), 0) - }) - t.Run("input encode.Status", func(t *testing.T) { - m := ×tamp.Timestamp{Seconds: time.Now().Unix()} - st, _ := ecode.Errorf(ecode.Unauthorized, "Unauthorized").WithDetails(m) - gst := status.New(codes.InvalidArgument, "requesterr") - gst, _ = gst.WithDetails(st.Proto()) - ec := ToEcode(gst) - - assert.Equal(t, int(ecode.Unauthorized), ec.Code()) - assert.Equal(t, "Unauthorized", ec.Message()) - assert.Len(t, ec.Details(), 1) - assert.IsType(t, m, ec.Details()[0]) - }) -} diff --git a/pkg/net/rpc/warden/logging.go b/pkg/net/rpc/warden/logging.go deleted file mode 100644 index 5c93aea5c..000000000 --- a/pkg/net/rpc/warden/logging.go +++ /dev/null @@ -1,174 +0,0 @@ -package warden - -import ( - "context" - "fmt" - "strconv" - "time" - - "google.golang.org/grpc" - "google.golang.org/grpc/peer" - - "github.com/go-kratos/kratos/pkg/ecode" - "github.com/go-kratos/kratos/pkg/log" - "github.com/go-kratos/kratos/pkg/net/metadata" -) - -// Warden Log Flag -const ( - // disable all log. - LogFlagDisable = 1 << iota - // disable print args on log. - LogFlagDisableArgs - // disable info level log. - LogFlagDisableInfo -) - -type logOption struct { - grpc.EmptyDialOption - grpc.EmptyCallOption - flag int8 -} - -// WithLogFlag disable client access log. -func WithLogFlag(flag int8) grpc.CallOption { - return logOption{flag: flag} -} - -// WithDialLogFlag set client level log behaviour. -func WithDialLogFlag(flag int8) grpc.DialOption { - return logOption{flag: flag} -} - -func extractLogCallOption(opts []grpc.CallOption) (flag int8) { - for _, opt := range opts { - if logOpt, ok := opt.(logOption); ok { - return logOpt.flag - } - } - return -} - -func extractLogDialOption(opts []grpc.DialOption) (flag int8) { - for _, opt := range opts { - if logOpt, ok := opt.(logOption); ok { - return logOpt.flag - } - } - return -} - -func logFn(code int, dt time.Duration) func(context.Context, ...log.D) { - switch { - case code < 0: - return log.Errorv - case dt >= time.Millisecond*500: - // TODO: slowlog make it configurable. - return log.Warnv - case code > 0: - return log.Warnv - } - return log.Infov -} - -// clientLogging warden grpc logging -func clientLogging(dialOptions ...grpc.DialOption) grpc.UnaryClientInterceptor { - defaultFlag := extractLogDialOption(dialOptions) - return func(ctx context.Context, method string, req, reply interface{}, cc *grpc.ClientConn, invoker grpc.UnaryInvoker, opts ...grpc.CallOption) error { - logFlag := extractLogCallOption(opts) | defaultFlag - - startTime := time.Now() - var peerInfo peer.Peer - opts = append(opts, grpc.Peer(&peerInfo)) - - // invoker requests - err := invoker(ctx, method, req, reply, cc, opts...) - - // after request - code := ecode.Cause(err).Code() - duration := time.Since(startTime) - // monitor - _metricClientReqDur.Observe(int64(duration/time.Millisecond), method) - _metricClientReqCodeTotal.Inc(method, strconv.Itoa(code)) - - if logFlag&LogFlagDisable != 0 { - return err - } - // TODO: find better way to deal with slow log. - if logFlag&LogFlagDisableInfo != 0 && err == nil && duration < 500*time.Millisecond { - return err - } - logFields := make([]log.D, 0, 7) - logFields = append(logFields, log.KVString("path", method)) - logFields = append(logFields, log.KVInt("ret", code)) - logFields = append(logFields, log.KVFloat64("ts", duration.Seconds())) - logFields = append(logFields, log.KVString("source", "grpc-access-log")) - if peerInfo.Addr != nil { - logFields = append(logFields, log.KVString("ip", peerInfo.Addr.String())) - } - if logFlag&LogFlagDisableArgs == 0 { - // TODO: it will panic if someone remove String method from protobuf message struct that auto generate from protoc. - logFields = append(logFields, log.KVString("args", req.(fmt.Stringer).String())) - } - if err != nil { - logFields = append(logFields, log.KVString("error", err.Error()), log.KVString("stack", fmt.Sprintf("%+v", err))) - } - logFn(code, duration)(ctx, logFields...) - return err - } -} - -// serverLogging warden grpc logging -func serverLogging(logFlag int8) grpc.UnaryServerInterceptor { - return func(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (interface{}, error) { - startTime := time.Now() - caller := metadata.String(ctx, metadata.Caller) - if caller == "" { - caller = "no_user" - } - var remoteIP string - if peerInfo, ok := peer.FromContext(ctx); ok { - remoteIP = peerInfo.Addr.String() - } - var quota float64 - if deadline, ok := ctx.Deadline(); ok { - quota = time.Until(deadline).Seconds() - } - - // call server handler - resp, err := handler(ctx, req) - - // after server response - code := ecode.Cause(err).Code() - duration := time.Since(startTime) - // monitor - _metricServerReqDur.Observe(int64(duration/time.Millisecond), info.FullMethod, caller) - _metricServerReqCodeTotal.Inc(info.FullMethod, caller, strconv.Itoa(code)) - - if logFlag&LogFlagDisable != 0 { - return resp, err - } - // TODO: find better way to deal with slow log. - if logFlag&LogFlagDisableInfo != 0 && err == nil && duration < 500*time.Millisecond { - return resp, err - } - logFields := []log.D{ - log.KVString("user", caller), - log.KVString("ip", remoteIP), - log.KVString("path", info.FullMethod), - log.KVInt("ret", code), - log.KVFloat64("ts", duration.Seconds()), - log.KVFloat64("timeout_quota", quota), - log.KVString("source", "grpc-access-log"), - } - if logFlag&LogFlagDisableArgs == 0 { - // TODO: it will panic if someone remove String method from protobuf message struct that auto generate from protoc. - logFields = append(logFields, log.KVString("args", req.(fmt.Stringer).String())) - } - if err != nil { - logFields = append(logFields, log.KVString("error", err.Error()), log.KVString("stack", fmt.Sprintf("%+v", err))) - } - logFn(code, duration)(ctx, logFields...) - return resp, err - } -} diff --git a/pkg/net/rpc/warden/logging_test.go b/pkg/net/rpc/warden/logging_test.go deleted file mode 100644 index 74ba602df..000000000 --- a/pkg/net/rpc/warden/logging_test.go +++ /dev/null @@ -1,294 +0,0 @@ -package warden - -import ( - "bytes" - "context" - "errors" - "io/ioutil" - "os" - "reflect" - "testing" - "time" - - "github.com/stretchr/testify/assert" - "google.golang.org/grpc" - - "github.com/go-kratos/kratos/pkg/log" -) - -func Test_logFn(t *testing.T) { - type args struct { - code int - dt time.Duration - } - tests := []struct { - name string - args args - want func(context.Context, ...log.D) - }{ - { - name: "ok", - args: args{code: 0, dt: time.Millisecond}, - want: log.Infov, - }, - { - name: "slowlog", - args: args{code: 0, dt: time.Second}, - want: log.Warnv, - }, - { - name: "business error", - args: args{code: 2233, dt: time.Millisecond}, - want: log.Warnv, - }, - { - name: "system error", - args: args{code: -1, dt: 0}, - want: log.Errorv, - }, - { - name: "system error and slowlog", - args: args{code: -1, dt: time.Second}, - want: log.Errorv, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - if got := logFn(tt.args.code, tt.args.dt); reflect.ValueOf(got).Pointer() != reflect.ValueOf(tt.want).Pointer() { - t.Errorf("unexpect log function!") - } - }) - } -} - -func callInterceptor(err error, interceptor grpc.UnaryClientInterceptor, opts ...grpc.CallOption) { - interceptor(context.Background(), - "test-method", - bytes.NewBufferString("test-req"), - "test_reply", - &grpc.ClientConn{}, - func(ctx context.Context, method string, req, reply interface{}, cc *grpc.ClientConn, opts ...grpc.CallOption) error { - return err - }, opts...) -} - -func TestClientLog(t *testing.T) { - stderr, err := ioutil.TempFile(os.TempDir(), "stderr") - if err != nil { - t.Fatal(err) - } - old := os.Stderr - os.Stderr = stderr - t.Logf("capture stderr file: %s", stderr.Name()) - - t.Run("test no option", func(t *testing.T) { - callInterceptor(nil, clientLogging()) - - stderr.Seek(0, os.SEEK_SET) - - data, err := ioutil.ReadAll(stderr) - if err != nil { - t.Error(err) - } - assert.Contains(t, string(data), "test-method") - assert.Contains(t, string(data), "test-req") - assert.Contains(t, string(data), "path") - assert.Contains(t, string(data), "ret") - assert.Contains(t, string(data), "ts") - assert.Contains(t, string(data), "grpc-access-log") - - stderr.Seek(0, os.SEEK_SET) - stderr.Truncate(0) - }) - - t.Run("test disable args", func(t *testing.T) { - callInterceptor(nil, clientLogging(WithDialLogFlag(LogFlagDisableArgs))) - - stderr.Seek(0, os.SEEK_SET) - - data, err := ioutil.ReadAll(stderr) - if err != nil { - t.Error(err) - } - assert.Contains(t, string(data), "test-method") - assert.NotContains(t, string(data), "test-req") - - stderr.Seek(0, os.SEEK_SET) - stderr.Truncate(0) - }) - - t.Run("test disable args and disable info", func(t *testing.T) { - callInterceptor(nil, clientLogging(WithDialLogFlag(LogFlagDisableArgs|LogFlagDisableInfo))) - callInterceptor(errors.New("test-error"), clientLogging(WithDialLogFlag(LogFlagDisableArgs|LogFlagDisableInfo))) - - stderr.Seek(0, os.SEEK_SET) - - data, err := ioutil.ReadAll(stderr) - if err != nil { - t.Error(err) - } - assert.Contains(t, string(data), "test-method") - assert.Contains(t, string(data), "test-error") - assert.NotContains(t, string(data), "INFO") - - stderr.Seek(0, os.SEEK_SET) - stderr.Truncate(0) - }) - - t.Run("test call option", func(t *testing.T) { - callInterceptor(nil, clientLogging(), WithLogFlag(LogFlagDisableArgs)) - - stderr.Seek(0, os.SEEK_SET) - - data, err := ioutil.ReadAll(stderr) - if err != nil { - t.Error(err) - } - assert.Contains(t, string(data), "test-method") - assert.NotContains(t, string(data), "test-req") - - stderr.Seek(0, os.SEEK_SET) - stderr.Truncate(0) - }) - - t.Run("test combine option", func(t *testing.T) { - interceptor := clientLogging(WithDialLogFlag(LogFlagDisableInfo)) - callInterceptor(nil, interceptor, WithLogFlag(LogFlagDisableArgs)) - callInterceptor(errors.New("test-error"), interceptor, WithLogFlag(LogFlagDisableArgs)) - - stderr.Seek(0, os.SEEK_SET) - - data, err := ioutil.ReadAll(stderr) - if err != nil { - t.Error(err) - } - assert.Contains(t, string(data), "test-method") - assert.Contains(t, string(data), "test-error") - assert.NotContains(t, string(data), "INFO") - - stderr.Seek(0, os.SEEK_SET) - stderr.Truncate(0) - }) - t.Run("test no log", func(t *testing.T) { - callInterceptor(errors.New("test error"), clientLogging(WithDialLogFlag(LogFlagDisable))) - stderr.Seek(0, os.SEEK_SET) - - data, err := ioutil.ReadAll(stderr) - if err != nil { - t.Error(err) - } - assert.Empty(t, data) - - stderr.Seek(0, os.SEEK_SET) - stderr.Truncate(0) - }) - - t.Run("test multi flag", func(t *testing.T) { - interceptor := clientLogging(WithDialLogFlag(LogFlagDisableInfo | LogFlagDisableArgs)) - callInterceptor(nil, interceptor) - callInterceptor(errors.New("test-error"), interceptor) - - stderr.Seek(0, os.SEEK_SET) - - data, err := ioutil.ReadAll(stderr) - if err != nil { - t.Error(err) - } - assert.Contains(t, string(data), "test-method") - assert.Contains(t, string(data), "test-error") - assert.NotContains(t, string(data), "INFO") - - stderr.Seek(0, os.SEEK_SET) - stderr.Truncate(0) - }) - os.Stderr = old -} - -func callServerInterceptor(err error, interceptor grpc.UnaryServerInterceptor) { - interceptor(context.Background(), - bytes.NewBufferString("test-req"), - &grpc.UnaryServerInfo{ - FullMethod: "test-method", - }, - func(ctx context.Context, req interface{}) (interface{}, error) { return nil, err }) -} - -func TestServerLog(t *testing.T) { - stderr, err := ioutil.TempFile(os.TempDir(), "stderr") - if err != nil { - t.Fatal(err) - } - old := os.Stderr - os.Stderr = stderr - t.Logf("capture stderr file: %s", stderr.Name()) - - t.Run("test no option", func(t *testing.T) { - callServerInterceptor(nil, serverLogging(0)) - - stderr.Seek(0, os.SEEK_SET) - - data, err := ioutil.ReadAll(stderr) - if err != nil { - t.Error(err) - } - assert.Contains(t, string(data), "test-method") - assert.Contains(t, string(data), "test-req") - assert.Contains(t, string(data), "path") - assert.Contains(t, string(data), "ret") - assert.Contains(t, string(data), "ts") - assert.Contains(t, string(data), "grpc-access-log") - - stderr.Seek(0, os.SEEK_SET) - stderr.Truncate(0) - }) - - t.Run("test disable args", func(t *testing.T) { - callServerInterceptor(nil, serverLogging(LogFlagDisableArgs)) - - stderr.Seek(0, os.SEEK_SET) - - data, err := ioutil.ReadAll(stderr) - if err != nil { - t.Error(err) - } - assert.Contains(t, string(data), "test-method") - assert.NotContains(t, string(data), "test-req") - - stderr.Seek(0, os.SEEK_SET) - stderr.Truncate(0) - }) - - t.Run("test no log", func(t *testing.T) { - callServerInterceptor(errors.New("test error"), serverLogging(LogFlagDisable)) - stderr.Seek(0, os.SEEK_SET) - - data, err := ioutil.ReadAll(stderr) - if err != nil { - t.Error(err) - } - assert.Empty(t, data) - - stderr.Seek(0, os.SEEK_SET) - stderr.Truncate(0) - }) - - t.Run("test multi flag", func(t *testing.T) { - interceptor := serverLogging(LogFlagDisableInfo | LogFlagDisableArgs) - callServerInterceptor(nil, interceptor) - callServerInterceptor(errors.New("test-error"), interceptor) - - stderr.Seek(0, os.SEEK_SET) - - data, err := ioutil.ReadAll(stderr) - if err != nil { - t.Error(err) - } - assert.Contains(t, string(data), "test-method") - assert.Contains(t, string(data), "test-error") - assert.NotContains(t, string(data), "INFO") - - stderr.Seek(0, os.SEEK_SET) - stderr.Truncate(0) - }) - os.Stderr = old -} diff --git a/pkg/net/rpc/warden/metrics.go b/pkg/net/rpc/warden/metrics.go deleted file mode 100644 index 3fd09bb3e..000000000 --- a/pkg/net/rpc/warden/metrics.go +++ /dev/null @@ -1,41 +0,0 @@ -package warden - -import "github.com/go-kratos/kratos/pkg/stat/metric" - -const ( - clientNamespace = "grpc_client" - serverNamespace = "grpc_server" -) - -var ( - _metricServerReqDur = metric.NewHistogramVec(&metric.HistogramVecOpts{ - Namespace: serverNamespace, - Subsystem: "requests", - Name: "duration_ms", - Help: "grpc server requests duration(ms).", - Labels: []string{"method", "caller"}, - Buckets: []float64{5, 10, 25, 50, 100, 250, 500, 1000}, - }) - _metricServerReqCodeTotal = metric.NewCounterVec(&metric.CounterVecOpts{ - Namespace: serverNamespace, - Subsystem: "requests", - Name: "code_total", - Help: "grpc server requests code count.", - Labels: []string{"method", "caller", "code"}, - }) - _metricClientReqDur = metric.NewHistogramVec(&metric.HistogramVecOpts{ - Namespace: clientNamespace, - Subsystem: "requests", - Name: "duration_ms", - Help: "grpc client requests duration(ms).", - Labels: []string{"method"}, - Buckets: []float64{5, 10, 25, 50, 100, 250, 500, 1000}, - }) - _metricClientReqCodeTotal = metric.NewCounterVec(&metric.CounterVecOpts{ - Namespace: clientNamespace, - Subsystem: "requests", - Name: "code_total", - Help: "grpc client requests code count.", - Labels: []string{"method", "code"}, - }) -) diff --git a/pkg/net/rpc/warden/ratelimiter/ratelimiter.go b/pkg/net/rpc/warden/ratelimiter/ratelimiter.go deleted file mode 100644 index 4696b5d5f..000000000 --- a/pkg/net/rpc/warden/ratelimiter/ratelimiter.go +++ /dev/null @@ -1,65 +0,0 @@ -package ratelimiter - -import ( - "context" - "sync/atomic" - "time" - - "google.golang.org/grpc" - - "github.com/go-kratos/kratos/pkg/log" - limit "github.com/go-kratos/kratos/pkg/ratelimit" - "github.com/go-kratos/kratos/pkg/ratelimit/bbr" - "github.com/go-kratos/kratos/pkg/stat/metric" -) - -var ( - _metricServerBBR = metric.NewCounterVec(&metric.CounterVecOpts{ - Namespace: "grpc_server", - Subsystem: "", - Name: "bbr_total", - Help: "grpc server bbr total.", - Labels: []string{"url"}, - }) -) - -// RateLimiter bbr middleware. -type RateLimiter struct { - group *bbr.Group - logTime int64 -} - -// New return a ratelimit middleware. -func New(conf *bbr.Config) (s *RateLimiter) { - return &RateLimiter{ - group: bbr.NewGroup(conf), - logTime: time.Now().UnixNano(), - } -} - -func (b *RateLimiter) printStats(fullMethod string, limiter limit.Limiter) { - now := time.Now().UnixNano() - if now-atomic.LoadInt64(&b.logTime) > int64(time.Second*3) { - atomic.StoreInt64(&b.logTime, now) - log.Info("grpc.bbr path:%s stat:%+v", fullMethod, limiter.(*bbr.BBR).Stat()) - } -} - -// Limit is a server interceptor that detects and rejects overloaded traffic. -func (b *RateLimiter) Limit() grpc.UnaryServerInterceptor { - return func(ctx context.Context, req interface{}, args *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (resp interface{}, err error) { - uri := args.FullMethod - limiter := b.group.Get(uri) - done, err := limiter.Allow(ctx) - if err != nil { - _metricServerBBR.Inc(uri) - return - } - defer func() { - done(limit.DoneInfo{Op: limit.Success}) - b.printStats(uri, limiter) - }() - resp, err = handler(ctx, req) - return - } -} diff --git a/pkg/net/rpc/warden/recovery.go b/pkg/net/rpc/warden/recovery.go deleted file mode 100644 index 7fa33144c..000000000 --- a/pkg/net/rpc/warden/recovery.go +++ /dev/null @@ -1,61 +0,0 @@ -package warden - -import ( - "context" - "fmt" - "os" - "runtime" - - "github.com/go-kratos/kratos/pkg/ecode" - "github.com/go-kratos/kratos/pkg/log" - - "google.golang.org/grpc" - "google.golang.org/grpc/codes" - "google.golang.org/grpc/status" -) - -// recovery is a server interceptor that recovers from any panics. -func (s *Server) recovery() grpc.UnaryServerInterceptor { - return func(ctx context.Context, req interface{}, args *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (resp interface{}, err error) { - defer func() { - if rerr := recover(); rerr != nil { - const size = 64 << 10 - buf := make([]byte, size) - rs := runtime.Stack(buf, false) - if rs > size { - rs = size - } - buf = buf[:rs] - pl := fmt.Sprintf("grpc server panic: %v\n%v\n%s\n", req, rerr, buf) - fmt.Fprint(os.Stderr, pl) - log.Error(pl) - err = status.Errorf(codes.Unknown, ecode.ServerErr.Error()) - } - }() - resp, err = handler(ctx, req) - return - } -} - -// recovery return a client interceptor that recovers from any panics. -func (c *Client) recovery() grpc.UnaryClientInterceptor { - return func(ctx context.Context, method string, req, reply interface{}, cc *grpc.ClientConn, invoker grpc.UnaryInvoker, opts ...grpc.CallOption) (err error) { - defer func() { - if rerr := recover(); rerr != nil { - const size = 64 << 10 - buf := make([]byte, size) - rs := runtime.Stack(buf, false) - if rs > size { - rs = size - } - buf = buf[:rs] - pl := fmt.Sprintf("grpc client panic: %v\n%v\n%v\n%s\n", req, reply, rerr, buf) - fmt.Fprintf(os.Stderr, pl) - log.Error(pl) - err = ecode.ServerErr - } - }() - err = invoker(ctx, method, req, reply, cc, opts...) - return - } -} diff --git a/pkg/net/rpc/warden/resolver/README.md b/pkg/net/rpc/warden/resolver/README.md deleted file mode 100644 index 456e74090..000000000 --- a/pkg/net/rpc/warden/resolver/README.md +++ /dev/null @@ -1,5 +0,0 @@ -#### warden/resolver - -##### 项目简介 - -warden 的 服务发现模块,用于从底层的注册中心中获取Server节点列表并返回给GRPC diff --git a/pkg/net/rpc/warden/resolver/direct/README.md b/pkg/net/rpc/warden/resolver/direct/README.md deleted file mode 100644 index 28f5d0ace..000000000 --- a/pkg/net/rpc/warden/resolver/direct/README.md +++ /dev/null @@ -1,6 +0,0 @@ -#### warden/resolver/direct - -##### 项目简介 - -warden 的直连服务模块,用于通过IP地址列表直接连接后端服务 -连接字符串格式: direct://default/192.168.1.1:8080,192.168.1.2:8081 diff --git a/pkg/net/rpc/warden/resolver/direct/direct.go b/pkg/net/rpc/warden/resolver/direct/direct.go deleted file mode 100644 index 6490b93c5..000000000 --- a/pkg/net/rpc/warden/resolver/direct/direct.go +++ /dev/null @@ -1,78 +0,0 @@ -package direct - -import ( - "context" - "fmt" - "strings" - - "github.com/go-kratos/kratos/pkg/conf/env" - "github.com/go-kratos/kratos/pkg/naming" - "github.com/go-kratos/kratos/pkg/net/rpc/warden/resolver" -) - -const ( - // Name is the name of direct resolver - Name = "direct" -) - -var _ naming.Resolver = &Direct{} - -// New return Direct -func New() *Direct { - return &Direct{} -} - -// Build build direct. -func Build(id string) *Direct { - return &Direct{id: id} -} - -// Direct is a resolver for conneting endpoints directly. -// example format: direct://default/192.168.1.1:8080,192.168.1.2:8081 -type Direct struct { - id string -} - -// Build direct build. -func (d *Direct) Build(id string, opt ...naming.BuildOpt) naming.Resolver { - return &Direct{id: id} -} - -// Scheme return the Scheme of Direct -func (d *Direct) Scheme() string { - return Name -} - -// Watch a tree. -func (d *Direct) Watch() <-chan struct{} { - ch := make(chan struct{}, 1) - ch <- struct{}{} - return ch -} - -// Unwatch a tree. -func (d *Direct) Unwatch(id string) { -} - -//Fetch fetch instances. -func (d *Direct) Fetch(ctx context.Context) (res *naming.InstancesInfo, found bool) { - var ins []*naming.Instance - addrs := strings.Split(d.id, ",") - for _, addr := range addrs { - ins = append(ins, &naming.Instance{ - Addrs: []string{fmt.Sprintf("%s://%s", resolver.Scheme, addr)}, - }) - } - if len(ins) > 0 { - found = true - } - res = &naming.InstancesInfo{ - Instances: map[string][]*naming.Instance{env.Zone: ins}, - } - return -} - -//Close close Direct -func (d *Direct) Close() error { - return nil -} diff --git a/pkg/net/rpc/warden/resolver/direct/test/direct_test.go b/pkg/net/rpc/warden/resolver/direct/test/direct_test.go deleted file mode 100644 index 690f3415a..000000000 --- a/pkg/net/rpc/warden/resolver/direct/test/direct_test.go +++ /dev/null @@ -1,85 +0,0 @@ -package direct - -import ( - "context" - "fmt" - "os" - "testing" - "time" - - "github.com/go-kratos/kratos/pkg/net/netutil/breaker" - "github.com/go-kratos/kratos/pkg/net/rpc/warden" - pb "github.com/go-kratos/kratos/pkg/net/rpc/warden/internal/proto/testproto" - "github.com/go-kratos/kratos/pkg/net/rpc/warden/resolver" - "github.com/go-kratos/kratos/pkg/net/rpc/warden/resolver/direct" - xtime "github.com/go-kratos/kratos/pkg/time" -) - -type testServer struct { - name string -} - -func (ts *testServer) SayHello(context.Context, *pb.HelloRequest) (*pb.HelloReply, error) { - return &pb.HelloReply{Message: ts.name, Success: true}, nil -} - -func (ts *testServer) StreamHello(ss pb.Greeter_StreamHelloServer) error { - panic("not implement error") -} - -func createServer(name, listen string) *warden.Server { - s := warden.NewServer(&warden.ServerConfig{Timeout: xtime.Duration(time.Second)}) - ts := &testServer{name} - pb.RegisterGreeterServer(s.Server(), ts) - go func() { - if err := s.Run(listen); err != nil { - panic(fmt.Sprintf("run warden server fail! err: %s", err)) - } - }() - return s -} - -func TestMain(m *testing.M) { - resolver.Register(direct.New()) - ctx := context.TODO() - s1 := createServer("server1", "127.0.0.1:18001") - s2 := createServer("server2", "127.0.0.1:18002") - defer s1.Shutdown(ctx) - defer s2.Shutdown(ctx) - os.Exit(m.Run()) -} - -func createTestClient(t *testing.T, connStr string) pb.GreeterClient { - client := warden.NewClient(&warden.ClientConfig{ - Dial: xtime.Duration(time.Second * 10), - Timeout: xtime.Duration(time.Second * 10), - Breaker: &breaker.Config{ - Window: xtime.Duration(3 * time.Second), - Bucket: 10, - Request: 20, - K: 1.5, - }, - }) - conn, err := client.Dial(context.TODO(), connStr) - if err != nil { - t.Fatalf("create client fail!err%s", err) - } - return pb.NewGreeterClient(conn) -} - -func TestDirect(t *testing.T) { - cli := createTestClient(t, "direct://default/127.0.0.1:18003,127.0.0.1:18002") - count := 0 - for i := 0; i < 10; i++ { - if resp, err := cli.SayHello(context.TODO(), &pb.HelloRequest{Age: 1, Name: "hello"}); err != nil { - t.Fatalf("TestDirect: SayHello failed!err:=%v", err) - } else { - if resp.Message == "server2" { - count++ - } - } - } - if count != 10 { - t.Fatalf("TestDirect: get server2 times must be 10") - } -} diff --git a/pkg/net/rpc/warden/resolver/resolver.go b/pkg/net/rpc/warden/resolver/resolver.go deleted file mode 100644 index e72d6352a..000000000 --- a/pkg/net/rpc/warden/resolver/resolver.go +++ /dev/null @@ -1,162 +0,0 @@ -package resolver - -import ( - "context" - "net/url" - "strconv" - "strings" - "sync" - - "github.com/go-kratos/kratos/pkg/conf/env" - "github.com/go-kratos/kratos/pkg/log" - "github.com/go-kratos/kratos/pkg/naming" - wmeta "github.com/go-kratos/kratos/pkg/net/rpc/warden/internal/metadata" - - "github.com/pkg/errors" - "google.golang.org/grpc/resolver" -) - -const ( - // Scheme is the scheme of discovery address - Scheme = "grpc" -) - -var ( - _ resolver.Resolver = &Resolver{} - _ resolver.Builder = &Builder{} - mu sync.Mutex -) - -// Register register resolver builder if nil. -func Register(b naming.Builder) { - mu.Lock() - defer mu.Unlock() - if resolver.Get(b.Scheme()) == nil { - resolver.Register(&Builder{b}) - } -} - -// Set override any registered builder -func Set(b naming.Builder) { - mu.Lock() - defer mu.Unlock() - resolver.Register(&Builder{b}) -} - -// Builder is also a resolver builder. -// It's build() function always returns itself. -type Builder struct { - naming.Builder -} - -// Build returns itself for Resolver, because it's both a builder and a resolver. -func (b *Builder) Build(target resolver.Target, cc resolver.ClientConn, opts resolver.BuildOptions) (resolver.Resolver, error) { - var zone = env.Zone - ss := int64(50) - clusters := map[string]struct{}{} - str := strings.SplitN(target.Endpoint, "?", 2) - if len(str) == 0 { - return nil, errors.Errorf("warden resolver: parse target.Endpoint(%s) failed!err:=endpoint is empty", target.Endpoint) - } else if len(str) == 2 { - m, err := url.ParseQuery(str[1]) - if err == nil { - for _, c := range m[naming.MetaCluster] { - clusters[c] = struct{}{} - } - zones := m[naming.MetaZone] - if len(zones) > 0 { - zone = zones[0] - } - if sub, ok := m["subset"]; ok { - if t, err := strconv.ParseInt(sub[0], 10, 64); err == nil { - ss = t - } - } - } - } - r := &Resolver{ - nr: b.Builder.Build(str[0], naming.Filter(Scheme, clusters), naming.ScheduleNode(zone), naming.Subset(int(ss))), - cc: cc, - quit: make(chan struct{}, 1), - zone: zone, - } - go r.updateproc() - return r, nil -} - -// Resolver watches for the updates on the specified target. -// Updates include address updates and service config updates. -type Resolver struct { - nr naming.Resolver - cc resolver.ClientConn - quit chan struct{} - - clusters map[string]struct{} - zone string - subsetSize int64 -} - -// Close is a noop for Resolver. -func (r *Resolver) Close() { - select { - case r.quit <- struct{}{}: - r.nr.Close() - default: - } -} - -// ResolveNow is a noop for Resolver. -func (r *Resolver) ResolveNow(o resolver.ResolveNowOptions) { -} - -func (r *Resolver) updateproc() { - event := r.nr.Watch() - for { - select { - case <-r.quit: - return - case _, ok := <-event: - if !ok { - return - } - } - if ins, ok := r.nr.Fetch(context.Background()); ok { - instances, _ := ins.Instances[r.zone] - if len(instances) == 0 { - for _, value := range ins.Instances { - instances = append(instances, value...) - } - } - r.newAddress(instances) - } - } -} - -func (r *Resolver) newAddress(instances []*naming.Instance) { - if len(instances) <= 0 { - return - } - addrs := make([]resolver.Address, 0, len(instances)) - for _, ins := range instances { - var weight int64 - if weight, _ = strconv.ParseInt(ins.Metadata[naming.MetaWeight], 10, 64); weight <= 0 { - weight = 10 - } - var rpc string - for _, a := range ins.Addrs { - u, err := url.Parse(a) - if err == nil && u.Scheme == Scheme { - rpc = u.Host - } - } - addr := resolver.Address{ - Addr: rpc, - Type: resolver.Backend, - ServerName: ins.AppID, - Metadata: wmeta.MD{Weight: uint64(weight), Color: ins.Metadata[naming.MetaColor]}, - } - addrs = append(addrs, addr) - } - log.Info("resolver: finally get %d instances", len(addrs)) - r.cc.NewAddress(addrs) -} diff --git a/pkg/net/rpc/warden/resolver/test/mockdiscovery.go b/pkg/net/rpc/warden/resolver/test/mockdiscovery.go deleted file mode 100644 index 586913049..000000000 --- a/pkg/net/rpc/warden/resolver/test/mockdiscovery.go +++ /dev/null @@ -1,87 +0,0 @@ -package resolver - -import ( - "context" - - "github.com/go-kratos/kratos/pkg/conf/env" - "github.com/go-kratos/kratos/pkg/naming" -) - -type mockDiscoveryBuilder struct { - instances map[string]*naming.Instance - watchch map[string][]*mockDiscoveryResolver -} - -func (mb *mockDiscoveryBuilder) Build(id string, opts ...naming.BuildOpt) naming.Resolver { - mr := &mockDiscoveryResolver{ - d: mb, - watchch: make(chan struct{}, 1), - } - mb.watchch[id] = append(mb.watchch[id], mr) - mr.watchch <- struct{}{} - return mr -} -func (mb *mockDiscoveryBuilder) Scheme() string { - return "mockdiscovery" -} - -type mockDiscoveryResolver struct { - d *mockDiscoveryBuilder - watchch chan struct{} -} - -var _ naming.Resolver = &mockDiscoveryResolver{} - -func (md *mockDiscoveryResolver) Fetch(ctx context.Context) (*naming.InstancesInfo, bool) { - zones := make(map[string][]*naming.Instance) - for _, v := range md.d.instances { - zones[v.Zone] = append(zones[v.Zone], v) - } - return &naming.InstancesInfo{Instances: zones}, len(zones) > 0 -} - -func (md *mockDiscoveryResolver) Watch() <-chan struct{} { - return md.watchch -} - -func (md *mockDiscoveryResolver) Close() error { - close(md.watchch) - return nil -} - -func (md *mockDiscoveryResolver) Scheme() string { - return "mockdiscovery" -} - -func (mb *mockDiscoveryBuilder) registry(appID string, hostname, rpc string, metadata map[string]string) { - ins := &naming.Instance{ - AppID: appID, - Env: "hello=world", - Hostname: hostname, - Addrs: []string{"grpc://" + rpc}, - Version: "1.1", - Zone: env.Zone, - Metadata: metadata, - } - mb.instances[hostname] = ins - if ch, ok := mb.watchch[appID]; ok { - var bullet struct{} - for _, c := range ch { - c.watchch <- bullet - } - } -} - -func (mb *mockDiscoveryBuilder) cancel(hostname string) { - ins, ok := mb.instances[hostname] - if !ok { - return - } - delete(mb.instances, hostname) - if ch, ok := mb.watchch[ins.AppID]; ok { - var bullet struct{} - for _, c := range ch { - c.watchch <- bullet - } - } -} diff --git a/pkg/net/rpc/warden/resolver/test/resovler_test.go b/pkg/net/rpc/warden/resolver/test/resovler_test.go deleted file mode 100644 index 9d763a653..000000000 --- a/pkg/net/rpc/warden/resolver/test/resovler_test.go +++ /dev/null @@ -1,314 +0,0 @@ -package resolver - -import ( - "context" - "fmt" - "os" - "testing" - "time" - - "github.com/go-kratos/kratos/pkg/conf/env" - "github.com/go-kratos/kratos/pkg/naming" - "github.com/go-kratos/kratos/pkg/net/netutil/breaker" - "github.com/go-kratos/kratos/pkg/net/rpc/warden" - pb "github.com/go-kratos/kratos/pkg/net/rpc/warden/internal/proto/testproto" - "github.com/go-kratos/kratos/pkg/net/rpc/warden/resolver" - xtime "github.com/go-kratos/kratos/pkg/time" - - "github.com/stretchr/testify/assert" -) - -var testServerMap map[string]*testServer - -func init() { - testServerMap = make(map[string]*testServer) -} - -const testAppID = "main.test" - -type testServer struct { - SayHelloCount int -} - -func resetCount() { - for _, s := range testServerMap { - s.SayHelloCount = 0 - } -} - -func (ts *testServer) SayHello(context.Context, *pb.HelloRequest) (*pb.HelloReply, error) { - ts.SayHelloCount++ - return &pb.HelloReply{Message: "hello", Success: true}, nil -} - -func (ts *testServer) StreamHello(ss pb.Greeter_StreamHelloServer) error { - panic("not implement error") -} - -func createServer(name, listen string) *warden.Server { - s := warden.NewServer(&warden.ServerConfig{Timeout: xtime.Duration(time.Second)}) - ts := &testServer{} - testServerMap[name] = ts - pb.RegisterGreeterServer(s.Server(), ts) - go func() { - if err := s.Run(listen); err != nil { - panic(fmt.Sprintf("run warden server fail! err: %s", err)) - } - }() - return s -} - -func NSayHello(c pb.GreeterClient, n int) func(*testing.T) { - return func(t *testing.T) { - for i := 0; i < n; i++ { - if _, err := c.SayHello(context.TODO(), &pb.HelloRequest{Age: 1, Name: "hello"}); err != nil { - t.Fatalf("call sayhello fail! err: %s", err) - } - } - } -} - -func createTestClient(t *testing.T) pb.GreeterClient { - client := warden.NewClient(&warden.ClientConfig{ - Dial: xtime.Duration(time.Second * 10), - Timeout: xtime.Duration(time.Second * 10), - Breaker: &breaker.Config{ - Window: xtime.Duration(3 * time.Second), - Bucket: 10, - Request: 20, - K: 1.5, - }, - }) - conn, err := client.Dial(context.TODO(), "mockdiscovery://authority/main.test") - if err != nil { - t.Fatalf("create client fail!err%s", err) - } - return pb.NewGreeterClient(conn) -} - -var mockResolver *mockDiscoveryBuilder - -func newMockDiscoveryBuilder() *mockDiscoveryBuilder { - return &mockDiscoveryBuilder{ - instances: make(map[string]*naming.Instance), - watchch: make(map[string][]*mockDiscoveryResolver), - } -} -func TestMain(m *testing.M) { - ctx := context.TODO() - mockResolver = newMockDiscoveryBuilder() - resolver.Set(mockResolver) - s1 := createServer("server1", "127.0.0.1:18081") - s2 := createServer("server2", "127.0.0.1:18082") - s3 := createServer("server3", "127.0.0.1:18083") - s4 := createServer("server4", "127.0.0.1:18084") - s5 := createServer("server5", "127.0.0.1:18085") - defer s1.Shutdown(ctx) - defer s2.Shutdown(ctx) - defer s3.Shutdown(ctx) - defer s4.Shutdown(ctx) - defer s5.Shutdown(ctx) - os.Exit(m.Run()) -} - -func TestAddResolver(t *testing.T) { - mockResolver.registry(testAppID, "server1", "127.0.0.1:18081", map[string]string{}) - c := createTestClient(t) - t.Run("test_say_hello", NSayHello(c, 10)) - assert.Equal(t, 10, testServerMap["server1"].SayHelloCount) - resetCount() -} - -func TestDeleteResolver(t *testing.T) { - mockResolver.registry(testAppID, "server1", "127.0.0.1:18081", map[string]string{}) - mockResolver.registry(testAppID, "server2", "127.0.0.1:18082", map[string]string{}) - c := createTestClient(t) - t.Run("test_say_hello", NSayHello(c, 10)) - assert.Equal(t, 10, testServerMap["server1"].SayHelloCount+testServerMap["server2"].SayHelloCount) - - mockResolver.cancel("server1") - resetCount() - time.Sleep(time.Millisecond * 10) - t.Run("test_say_hello", NSayHello(c, 10)) - assert.Equal(t, 0, testServerMap["server1"].SayHelloCount) - - resetCount() -} - -func TestUpdateResolver(t *testing.T) { - mockResolver.registry(testAppID, "server1", "127.0.0.1:18081", map[string]string{}) - mockResolver.registry(testAppID, "server2", "127.0.0.1:18082", map[string]string{}) - - c := createTestClient(t) - t.Run("test_say_hello", NSayHello(c, 10)) - assert.Equal(t, 10, testServerMap["server1"].SayHelloCount+testServerMap["server2"].SayHelloCount) - - mockResolver.registry(testAppID, "server1", "127.0.0.1:18083", map[string]string{}) - mockResolver.registry(testAppID, "server2", "127.0.0.1:18084", map[string]string{}) - resetCount() - time.Sleep(time.Millisecond * 10) - t.Run("test_say_hello", NSayHello(c, 10)) - assert.Equal(t, 0, testServerMap["server1"].SayHelloCount+testServerMap["server2"].SayHelloCount) - assert.Equal(t, 10, testServerMap["server3"].SayHelloCount+testServerMap["server4"].SayHelloCount) - - resetCount() -} - -func TestErrorResolver(t *testing.T) { - mockResolver := newMockDiscoveryBuilder() - resolver.Set(mockResolver) - mockResolver.registry(testAppID, "server1", "127.0.0.1:18081", map[string]string{}) - mockResolver.registry(testAppID, "server6", "127.0.0.1:18086", map[string]string{}) - - c := createTestClient(t) - t.Run("test_say_hello", NSayHello(c, 10)) - assert.Equal(t, 10, testServerMap["server1"].SayHelloCount) - - resetCount() -} - -// FIXME -func testClusterResolver(t *testing.T) { - mockResolver := newMockDiscoveryBuilder() - resolver.Set(mockResolver) - mockResolver.registry(testAppID, "server1", "127.0.0.1:18081", map[string]string{"cluster": "c1"}) - mockResolver.registry(testAppID, "server2", "127.0.0.1:18082", map[string]string{"cluster": "c1"}) - mockResolver.registry(testAppID, "server3", "127.0.0.1:18083", map[string]string{"cluster": "c2"}) - mockResolver.registry(testAppID, "server4", "127.0.0.1:18084", map[string]string{}) - mockResolver.registry(testAppID, "server5", "127.0.0.1:18084", map[string]string{}) - - client := warden.NewClient(&warden.ClientConfig{Clusters: []string{"c1"}}) - conn, err := client.Dial(context.TODO(), "mockdiscovery://authority/main.test?cluster=c2") - if err != nil { - t.Fatalf("create client fail!err%s", err) - } - time.Sleep(time.Millisecond * 10) - cli := pb.NewGreeterClient(conn) - if _, err := cli.SayHello(context.TODO(), &pb.HelloRequest{Age: 1, Name: "hello"}); err != nil { - t.Fatalf("call sayhello fail! err: %s", err) - } - if _, err := cli.SayHello(context.TODO(), &pb.HelloRequest{Age: 1, Name: "hello"}); err != nil { - t.Fatalf("call sayhello fail! err: %s", err) - } - if _, err := cli.SayHello(context.TODO(), &pb.HelloRequest{Age: 1, Name: "hello"}); err != nil { - t.Fatalf("call sayhello fail! err: %s", err) - } - assert.Equal(t, 1, testServerMap["server1"].SayHelloCount) - assert.Equal(t, 1, testServerMap["server2"].SayHelloCount) - assert.Equal(t, 1, testServerMap["server3"].SayHelloCount) - - resetCount() -} - -// FIXME -func testNoClusterResolver(t *testing.T) { - mockResolver := newMockDiscoveryBuilder() - resolver.Set(mockResolver) - mockResolver.registry(testAppID, "server1", "127.0.0.1:18081", map[string]string{"cluster": "c1"}) - mockResolver.registry(testAppID, "server2", "127.0.0.1:18082", map[string]string{"cluster": "c1"}) - mockResolver.registry(testAppID, "server3", "127.0.0.1:18083", map[string]string{"cluster": "c2"}) - mockResolver.registry(testAppID, "server4", "127.0.0.1:18084", map[string]string{}) - client := warden.NewClient(&warden.ClientConfig{}) - conn, err := client.Dial(context.TODO(), "mockdiscovery://authority/main.test") - if err != nil { - t.Fatalf("create client fail!err%s", err) - } - time.Sleep(time.Millisecond * 20) - cli := pb.NewGreeterClient(conn) - if _, err := cli.SayHello(context.TODO(), &pb.HelloRequest{Age: 1, Name: "hello"}); err != nil { - t.Fatalf("call sayhello fail! err: %s", err) - } - if _, err := cli.SayHello(context.TODO(), &pb.HelloRequest{Age: 1, Name: "hello"}); err != nil { - t.Fatalf("call sayhello fail! err: %s", err) - } - if _, err := cli.SayHello(context.TODO(), &pb.HelloRequest{Age: 1, Name: "hello"}); err != nil { - t.Fatalf("call sayhello fail! err: %s", err) - } - if _, err := cli.SayHello(context.TODO(), &pb.HelloRequest{Age: 1, Name: "hello"}); err != nil { - t.Fatalf("call sayhello fail! err: %s", err) - } - assert.Equal(t, 1, testServerMap["server1"].SayHelloCount) - assert.Equal(t, 1, testServerMap["server2"].SayHelloCount) - assert.Equal(t, 1, testServerMap["server3"].SayHelloCount) - assert.Equal(t, 1, testServerMap["server4"].SayHelloCount) - - resetCount() -} - -func TestZoneResolver(t *testing.T) { - mockResolver := newMockDiscoveryBuilder() - resolver.Set(mockResolver) - mockResolver.registry(testAppID, "server1", "127.0.0.1:18081", map[string]string{}) - env.Zone = "testsh" - mockResolver.registry(testAppID, "server2", "127.0.0.1:18082", map[string]string{}) - env.Zone = "hhhh" - client := warden.NewClient(&warden.ClientConfig{Zone: "testsh"}) - conn, err := client.Dial(context.TODO(), "mockdiscovery://authority/main.test") - if err != nil { - t.Fatalf("create client fail!err%s", err) - } - time.Sleep(time.Millisecond * 10) - cli := pb.NewGreeterClient(conn) - if _, err := cli.SayHello(context.TODO(), &pb.HelloRequest{Age: 1, Name: "hello"}); err != nil { - t.Fatalf("call sayhello fail! err: %s", err) - } - if _, err := cli.SayHello(context.TODO(), &pb.HelloRequest{Age: 1, Name: "hello"}); err != nil { - t.Fatalf("call sayhello fail! err: %s", err) - } - if _, err := cli.SayHello(context.TODO(), &pb.HelloRequest{Age: 1, Name: "hello"}); err != nil { - t.Fatalf("call sayhello fail! err: %s", err) - } - assert.Equal(t, 0, testServerMap["server1"].SayHelloCount) - assert.Equal(t, 3, testServerMap["server2"].SayHelloCount) - - resetCount() -} - -// FIXME -func testSubsetConn(t *testing.T) { - mockResolver := newMockDiscoveryBuilder() - resolver.Set(mockResolver) - mockResolver.registry(testAppID, "server1", "127.0.0.1:18081", map[string]string{}) - mockResolver.registry(testAppID, "server2", "127.0.0.1:18082", map[string]string{}) - mockResolver.registry(testAppID, "server3", "127.0.0.1:18083", map[string]string{}) - mockResolver.registry(testAppID, "server4", "127.0.0.1:18084", map[string]string{}) - mockResolver.registry(testAppID, "server5", "127.0.0.1:18085", map[string]string{}) - - client := warden.NewClient(nil) - conn, err := client.Dial(context.TODO(), "mockdiscovery://authority/main.test?subset=3") - if err != nil { - t.Fatalf("create client fail!err%s", err) - } - time.Sleep(time.Millisecond * 20) - cli := pb.NewGreeterClient(conn) - for i := 0; i < 6; i++ { - if _, err := cli.SayHello(context.TODO(), &pb.HelloRequest{Age: 1, Name: "hello"}); err != nil { - t.Fatalf("call sayhello fail! err: %s", err) - } - } - assert.Equal(t, 2, testServerMap["server2"].SayHelloCount) - assert.Equal(t, 2, testServerMap["server5"].SayHelloCount) - assert.Equal(t, 2, testServerMap["server4"].SayHelloCount) - resetCount() - mockResolver.cancel("server4") - time.Sleep(time.Millisecond * 20) - for i := 0; i < 6; i++ { - if _, err := cli.SayHello(context.TODO(), &pb.HelloRequest{Age: 1, Name: "hello"}); err != nil { - t.Fatalf("call sayhello fail! err: %s", err) - } - } - assert.Equal(t, 2, testServerMap["server5"].SayHelloCount) - assert.Equal(t, 2, testServerMap["server2"].SayHelloCount) - assert.Equal(t, 2, testServerMap["server3"].SayHelloCount) - resetCount() - mockResolver.registry(testAppID, "server4", "127.0.0.1:18084", map[string]string{}) - time.Sleep(time.Millisecond * 20) - for i := 0; i < 6; i++ { - if _, err := cli.SayHello(context.TODO(), &pb.HelloRequest{Age: 1, Name: "hello"}); err != nil { - t.Fatalf("call sayhello fail! err: %s", err) - } - } - assert.Equal(t, 2, testServerMap["server2"].SayHelloCount) - assert.Equal(t, 2, testServerMap["server5"].SayHelloCount) - assert.Equal(t, 2, testServerMap["server4"].SayHelloCount) -} diff --git a/pkg/net/rpc/warden/resolver/util.go b/pkg/net/rpc/warden/resolver/util.go deleted file mode 100644 index 73df667c3..000000000 --- a/pkg/net/rpc/warden/resolver/util.go +++ /dev/null @@ -1,16 +0,0 @@ -package resolver - -import ( - "flag" - "fmt" -) - -// RegisterTarget will register grpc discovery mock address flag -func RegisterTarget(target *string, discoveryID string) { - flag.CommandLine.StringVar( - target, - fmt.Sprintf("grpc.%s", discoveryID), - fmt.Sprintf("discovery://default/%s", discoveryID), - fmt.Sprintf("App's grpc target.\n example: -grpc.%s=\"127.0.0.1:9090\"", discoveryID), - ) -} diff --git a/pkg/net/rpc/warden/server.go b/pkg/net/rpc/warden/server.go deleted file mode 100644 index 3d59beb7d..000000000 --- a/pkg/net/rpc/warden/server.go +++ /dev/null @@ -1,362 +0,0 @@ -package warden - -import ( - "context" - "flag" - "fmt" - "math" - "net" - "os" - "sync" - "time" - - "github.com/go-kratos/kratos/pkg/conf/dsn" - "github.com/go-kratos/kratos/pkg/log" - nmd "github.com/go-kratos/kratos/pkg/net/metadata" - "github.com/go-kratos/kratos/pkg/net/rpc/warden/ratelimiter" - "github.com/go-kratos/kratos/pkg/net/trace" - xtime "github.com/go-kratos/kratos/pkg/time" - - //this package is for json format response - _ "github.com/go-kratos/kratos/pkg/net/rpc/warden/internal/encoding/json" - "github.com/go-kratos/kratos/pkg/net/rpc/warden/internal/status" - - "github.com/pkg/errors" - "google.golang.org/grpc" - _ "google.golang.org/grpc/encoding/gzip" // NOTE: use grpc gzip by header grpc-accept-encoding - "google.golang.org/grpc/keepalive" - "google.golang.org/grpc/metadata" - "google.golang.org/grpc/peer" - "google.golang.org/grpc/reflection" -) - -var ( - _grpcDSN string - _defaultSerConf = &ServerConfig{ - Network: "tcp", - Addr: "0.0.0.0:9000", - Timeout: xtime.Duration(time.Second), - IdleTimeout: xtime.Duration(time.Second * 180), - MaxLifeTime: xtime.Duration(time.Hour * 2), - ForceCloseWait: xtime.Duration(time.Second * 20), - KeepAliveInterval: xtime.Duration(time.Second * 60), - KeepAliveTimeout: xtime.Duration(time.Second * 20), - } - _abortIndex int8 = math.MaxInt8 / 2 -) - -// ServerConfig is rpc server conf. -type ServerConfig struct { - // Network is grpc listen network,default value is tcp - Network string `dsn:"network"` - // Addr is grpc listen addr,default value is 0.0.0.0:9000 - Addr string `dsn:"address"` - // Timeout is context timeout for per rpc call. - Timeout xtime.Duration `dsn:"query.timeout"` - // IdleTimeout is a duration for the amount of time after which an idle connection would be closed by sending a GoAway. - // Idleness duration is defined since the most recent time the number of outstanding RPCs became zero or the connection establishment. - IdleTimeout xtime.Duration `dsn:"query.idleTimeout"` - // MaxLifeTime is a duration for the maximum amount of time a connection may exist before it will be closed by sending a GoAway. - // A random jitter of +/-10% will be added to MaxConnectionAge to spread out connection storms. - MaxLifeTime xtime.Duration `dsn:"query.maxLife"` - // ForceCloseWait is an additive period after MaxLifeTime after which the connection will be forcibly closed. - ForceCloseWait xtime.Duration `dsn:"query.closeWait"` - // KeepAliveInterval is after a duration of this time if the server doesn't see any activity it pings the client to see if the transport is still alive. - KeepAliveInterval xtime.Duration `dsn:"query.keepaliveInterval"` - // KeepAliveTimeout is After having pinged for keepalive check, the server waits for a duration of Timeout and if no activity is seen even after that - // the connection is closed. - KeepAliveTimeout xtime.Duration `dsn:"query.keepaliveTimeout"` - // LogFlag to control log behaviour. e.g. LogFlag: warden.LogFlagDisableLog. - // Disable: 1 DisableArgs: 2 DisableInfo: 4 - LogFlag int8 `dsn:"query.logFlag"` -} - -// Server is the framework's server side instance, it contains the GrpcServer, interceptor and interceptors. -// Create an instance of Server, by using NewServer(). -type Server struct { - conf *ServerConfig - mutex sync.RWMutex - - server *grpc.Server - handlers []grpc.UnaryServerInterceptor -} - -// handle return a new unary server interceptor for OpenTracing\Logging\LinkTimeout. -func (s *Server) handle() grpc.UnaryServerInterceptor { - return func(ctx context.Context, req interface{}, args *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (resp interface{}, err error) { - var ( - cancel func() - addr string - ) - s.mutex.RLock() - conf := s.conf - s.mutex.RUnlock() - // get derived timeout from grpc context, - // compare with the warden configured, - // and use the minimum one - timeout := time.Duration(conf.Timeout) - if dl, ok := ctx.Deadline(); ok { - ctimeout := time.Until(dl) - if ctimeout-time.Millisecond*20 > 0 { - ctimeout = ctimeout - time.Millisecond*20 - } - if timeout > ctimeout { - timeout = ctimeout - } - } - ctx, cancel = context.WithTimeout(ctx, timeout) - defer cancel() - - // get grpc metadata(trace & remote_ip & color) - var t trace.Trace - cmd := nmd.MD{} - if gmd, ok := metadata.FromIncomingContext(ctx); ok { - t, _ = trace.Extract(trace.GRPCFormat, gmd) - for key, vals := range gmd { - if nmd.IsIncomingKey(key) { - cmd[key] = vals[0] - } - } - } - if t == nil { - t = trace.New(args.FullMethod) - } else { - t.SetTitle(args.FullMethod) - } - - if pr, ok := peer.FromContext(ctx); ok { - addr = pr.Addr.String() - t.SetTag(trace.String(trace.TagAddress, addr)) - } - defer t.Finish(&err) - - // use common meta data context instead of grpc context - ctx = nmd.NewContext(ctx, cmd) - ctx = trace.NewContext(ctx, t) - - resp, err = handler(ctx, req) - return resp, status.FromError(err).Err() - } -} - -func init() { - addFlag(flag.CommandLine) -} - -func addFlag(fs *flag.FlagSet) { - v := os.Getenv("GRPC") - if v == "" { - v = "tcp://0.0.0.0:9000/?timeout=1s&idle_timeout=60s" - } - fs.StringVar(&_grpcDSN, "grpc", v, "listen grpc dsn, or use GRPC env variable.") - fs.Var(&_grpcTarget, "grpc.target", "usage: -grpc.target=seq.service=127.0.0.1:9000 -grpc.target=fav.service=192.168.10.1:9000") -} - -func parseDSN(rawdsn string) *ServerConfig { - conf := new(ServerConfig) - d, err := dsn.Parse(rawdsn) - if err != nil { - panic(errors.WithMessage(err, fmt.Sprintf("warden: invalid dsn: %s", rawdsn))) - } - if _, err = d.Bind(conf); err != nil { - panic(errors.WithMessage(err, fmt.Sprintf("warden: invalid dsn: %s", rawdsn))) - } - return conf -} - -// NewServer returns a new blank Server instance with a default server interceptor. -func NewServer(conf *ServerConfig, opt ...grpc.ServerOption) (s *Server) { - if conf == nil { - if !flag.Parsed() { - fmt.Fprint(os.Stderr, "[warden] please call flag.Parse() before Init warden server, some configure may not effect\n") - } - conf = parseDSN(_grpcDSN) - } else { - fmt.Fprintf(os.Stderr, "[warden] config is Deprecated, argument will be ignored. please use -grpc flag or GRPC env to configure warden server.\n") - } - s = new(Server) - if err := s.SetConfig(conf); err != nil { - panic(errors.Errorf("warden: set config failed!err: %s", err.Error())) - } - keepParam := grpc.KeepaliveParams(keepalive.ServerParameters{ - MaxConnectionIdle: time.Duration(s.conf.IdleTimeout), - MaxConnectionAgeGrace: time.Duration(s.conf.ForceCloseWait), - Time: time.Duration(s.conf.KeepAliveInterval), - Timeout: time.Duration(s.conf.KeepAliveTimeout), - MaxConnectionAge: time.Duration(s.conf.MaxLifeTime), - }) - opt = append(opt, keepParam, grpc.UnaryInterceptor(s.interceptor)) - s.server = grpc.NewServer(opt...) - s.Use(s.recovery(), s.handle(), serverLogging(conf.LogFlag), s.stats(), s.validate()) - s.Use(ratelimiter.New(nil).Limit()) - return -} - -// SetConfig hot reloads server config -func (s *Server) SetConfig(conf *ServerConfig) (err error) { - if conf == nil { - conf = _defaultSerConf - } - if conf.Timeout <= 0 { - conf.Timeout = xtime.Duration(time.Second) - } - if conf.IdleTimeout <= 0 { - conf.IdleTimeout = xtime.Duration(time.Second * 60) - } - if conf.MaxLifeTime <= 0 { - conf.MaxLifeTime = xtime.Duration(time.Hour * 2) - } - if conf.ForceCloseWait <= 0 { - conf.ForceCloseWait = xtime.Duration(time.Second * 20) - } - if conf.KeepAliveInterval <= 0 { - conf.KeepAliveInterval = xtime.Duration(time.Second * 60) - } - if conf.KeepAliveTimeout <= 0 { - conf.KeepAliveTimeout = xtime.Duration(time.Second * 20) - } - if conf.Addr == "" { - conf.Addr = "0.0.0.0:9000" - } - if conf.Network == "" { - conf.Network = "tcp" - } - s.mutex.Lock() - s.conf = conf - s.mutex.Unlock() - return nil -} - -// interceptor is a single interceptor out of a chain of many interceptors. -// Execution is done in left-to-right order, including passing of context. -// For example ChainUnaryServer(one, two, three) will execute one before two before three, and three -// will see context changes of one and two. -func (s *Server) interceptor(ctx context.Context, req interface{}, args *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (interface{}, error) { - var ( - i int - chain grpc.UnaryHandler - ) - - n := len(s.handlers) - if n == 0 { - return handler(ctx, req) - } - - chain = func(ic context.Context, ir interface{}) (interface{}, error) { - if i == n-1 { - return handler(ic, ir) - } - i++ - return s.handlers[i](ic, ir, args, chain) - } - - return s.handlers[0](ctx, req, args, chain) -} - -// Server return the grpc server for registering service. -func (s *Server) Server() *grpc.Server { - return s.server -} - -// Use attachs a global inteceptor to the server. -// For example, this is the right place for a rate limiter or error management inteceptor. -func (s *Server) Use(handlers ...grpc.UnaryServerInterceptor) *Server { - finalSize := len(s.handlers) + len(handlers) - if finalSize >= int(_abortIndex) { - panic("warden: server use too many handlers") - } - mergedHandlers := make([]grpc.UnaryServerInterceptor, finalSize) - copy(mergedHandlers, s.handlers) - copy(mergedHandlers[len(s.handlers):], handlers) - s.handlers = mergedHandlers - return s -} - -// Run create a tcp listener and start goroutine for serving each incoming request. -// Run will return a non-nil error unless Stop or GracefulStop is called. -func (s *Server) Run(addr string) error { - lis, err := net.Listen("tcp", addr) - if err != nil { - err = errors.WithStack(err) - log.Error("failed to listen: %v", err) - return err - } - reflection.Register(s.server) - return s.Serve(lis) -} - -// RunUnix create a unix listener and start goroutine for serving each incoming request. -// RunUnix will return a non-nil error unless Stop or GracefulStop is called. -func (s *Server) RunUnix(file string) error { - lis, err := net.Listen("unix", file) - if err != nil { - err = errors.WithStack(err) - log.Error("failed to listen: %v", err) - return err - } - reflection.Register(s.server) - return s.Serve(lis) -} - -// Start create a new goroutine run server with configured listen addr -// will panic if any error happened -// return server itself -func (s *Server) Start() (*Server, error) { - _, err := s.startWithAddr() - if err != nil { - return nil, err - } - return s, nil -} - -// StartWithAddr create a new goroutine run server with configured listen addr -// will panic if any error happened -// return server itself and the actually listened address (if configured listen -// port is zero, the os will allocate an unused port) -func (s *Server) StartWithAddr() (*Server, net.Addr, error) { - addr, err := s.startWithAddr() - if err != nil { - return nil, nil, err - } - return s, addr, nil -} - -func (s *Server) startWithAddr() (net.Addr, error) { - lis, err := net.Listen(s.conf.Network, s.conf.Addr) - if err != nil { - return nil, err - } - log.Info("warden: start grpc listen addr: %v", lis.Addr()) - reflection.Register(s.server) - go func() { - if err := s.Serve(lis); err != nil { - panic(err) - } - }() - return lis.Addr(), nil -} - -// Serve accepts incoming connections on the listener lis, creating a new -// ServerTransport and service goroutine for each. -// Serve will return a non-nil error unless Stop or GracefulStop is called. -func (s *Server) Serve(lis net.Listener) error { - return s.server.Serve(lis) -} - -// Shutdown stops the server gracefully. It stops the server from -// accepting new connections and RPCs and blocks until all the pending RPCs are -// finished or the context deadline is reached. -func (s *Server) Shutdown(ctx context.Context) (err error) { - ch := make(chan struct{}) - go func() { - s.server.GracefulStop() - close(ch) - }() - select { - case <-ctx.Done(): - s.server.Stop() - err = ctx.Err() - case <-ch: - } - return -} diff --git a/pkg/net/rpc/warden/server_test.go b/pkg/net/rpc/warden/server_test.go deleted file mode 100644 index 19f09fc9d..000000000 --- a/pkg/net/rpc/warden/server_test.go +++ /dev/null @@ -1,605 +0,0 @@ -package warden - -import ( - "context" - "fmt" - "io" - "math/rand" - "net" - "os" - "strconv" - "strings" - "sync" - "testing" - "time" - - "github.com/go-kratos/kratos/pkg/ecode" - "github.com/go-kratos/kratos/pkg/log" - nmd "github.com/go-kratos/kratos/pkg/net/metadata" - "github.com/go-kratos/kratos/pkg/net/netutil/breaker" - pb "github.com/go-kratos/kratos/pkg/net/rpc/warden/internal/proto/testproto" - xtrace "github.com/go-kratos/kratos/pkg/net/trace" - xtime "github.com/go-kratos/kratos/pkg/time" - - "github.com/pkg/errors" - "github.com/stretchr/testify/assert" - "google.golang.org/grpc" - "google.golang.org/grpc/codes" - "google.golang.org/grpc/status" -) - -const ( - _separator = "\001" - - _testAddr = "127.0.0.1:9090" -) - -var ( - outPut []string - _testOnce sync.Once - server *Server - - clientConfig = ClientConfig{ - Dial: xtime.Duration(time.Second * 10), - Timeout: xtime.Duration(time.Second * 10), - Breaker: &breaker.Config{ - Window: xtime.Duration(3 * time.Second), - Bucket: 10, - K: 1.5, - }, - } - clientConfig2 = ClientConfig{ - Dial: xtime.Duration(time.Second * 10), - Timeout: xtime.Duration(time.Second * 10), - Breaker: &breaker.Config{ - Window: xtime.Duration(3 * time.Second), - Bucket: 10, - Request: 20, - K: 1.5, - }, - Method: map[string]*ClientConfig{`/testproto.Greeter/SayHello`: {Timeout: xtime.Duration(time.Millisecond * 200)}}, - } -) - -type helloServer struct { - t *testing.T -} - -func (s *helloServer) SayHello(ctx context.Context, in *pb.HelloRequest) (*pb.HelloReply, error) { - if in.Name == "trace_test" { - t, isok := xtrace.FromContext(ctx) - if !isok { - t = xtrace.New("test title") - s.t.Fatalf("no trace extracted from server context") - } - newCtx := xtrace.NewContext(ctx, t) - if in.Age == 0 { - runClient(newCtx, &clientConfig, s.t, "trace_test", 1) - } - } else if in.Name == "recovery_test" { - panic("test recovery") - } else if in.Name == "graceful_shutdown" { - time.Sleep(time.Second * 3) - } else if in.Name == "timeout_test" { - if in.Age > 10 { - s.t.Fatalf("can not deliver requests over 10 times because of link timeout") - return &pb.HelloReply{Message: "Hello " + in.Name, Success: true}, nil - } - time.Sleep(time.Millisecond * 10) - _, err := runClient(ctx, &clientConfig, s.t, "timeout_test", in.Age+1) - return &pb.HelloReply{Message: "Hello " + in.Name, Success: true}, err - } else if in.Name == "timeout_test2" { - if in.Age > 10 { - s.t.Fatalf("can not deliver requests over 10 times because of link timeout") - return &pb.HelloReply{Message: "Hello " + in.Name, Success: true}, nil - } - time.Sleep(time.Millisecond * 10) - _, err := runClient(ctx, &clientConfig2, s.t, "timeout_test2", in.Age+1) - return &pb.HelloReply{Message: "Hello " + in.Name, Success: true}, err - } else if in.Name == "color_test" { - if in.Age == 0 { - resp, err := runClient(ctx, &clientConfig, s.t, "color_test", in.Age+1) - return resp, err - } - color := nmd.String(ctx, nmd.Color) - return &pb.HelloReply{Message: "Hello " + color, Success: true}, nil - } else if in.Name == "breaker_test" { - if rand.Intn(100) <= 50 { - return nil, status.Errorf(codes.ResourceExhausted, "test") - } - return &pb.HelloReply{Message: "Hello " + in.Name, Success: true}, nil - } else if in.Name == "error_detail" { - err, _ := ecode.Error(ecode.Code(123456), "test_error_detail").WithDetails(&pb.HelloReply{Success: true}) - return nil, err - } else if in.Name == "ecode_status" { - reply := &pb.HelloReply{Message: "status", Success: true} - st, _ := ecode.Error(ecode.RequestErr, "RequestErr").WithDetails(reply) - return nil, st - } else if in.Name == "general_error" { - return nil, fmt.Errorf("haha is error") - } else if in.Name == "ecode_code_error" { - return nil, ecode.Conflict - } else if in.Name == "pb_error_error" { - return nil, ecode.Error(ecode.Code(11122), "haha") - } else if in.Name == "ecode_status_error" { - return nil, ecode.Error(ecode.RequestErr, "RequestErr") - } else if in.Name == "test_remote_port" { - if strconv.Itoa(int(in.Age)) != nmd.String(ctx, nmd.RemotePort) { - return nil, fmt.Errorf("error port %d", in.Age) - } - reply := &pb.HelloReply{Message: "status", Success: true} - return reply, nil - } else if in.Name == "time_opt" { - time.Sleep(time.Second) - reply := &pb.HelloReply{Message: "status", Success: true} - return reply, nil - } - - return &pb.HelloReply{Message: "Hello " + in.Name, Success: true}, nil -} - -func (s *helloServer) StreamHello(ss pb.Greeter_StreamHelloServer) error { - for i := 0; i < 3; i++ { - in, err := ss.Recv() - if err == io.EOF { - return nil - } - if err != nil { - return err - } - ret := &pb.HelloReply{Message: "Hello " + in.Name, Success: true} - err = ss.Send(ret) - if err != nil { - return err - } - } - return nil -} - -func runServer(t *testing.T, interceptors ...grpc.UnaryServerInterceptor) func() { - return func() { - server = NewServer(&ServerConfig{Addr: _testAddr, Timeout: xtime.Duration(time.Second)}) - pb.RegisterGreeterServer(server.Server(), &helloServer{t}) - server.Use( - func(ctx context.Context, req interface{}, args *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (interface{}, error) { - outPut = append(outPut, "1") - resp, err := handler(ctx, req) - outPut = append(outPut, "2") - return resp, err - }, - func(ctx context.Context, req interface{}, args *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (interface{}, error) { - outPut = append(outPut, "3") - resp, err := handler(ctx, req) - outPut = append(outPut, "4") - return resp, err - }) - if _, err := server.Start(); err != nil { - t.Fatal(err) - } - } -} - -func runClient(ctx context.Context, cc *ClientConfig, t *testing.T, name string, age int32, interceptors ...grpc.UnaryClientInterceptor) (resp *pb.HelloReply, err error) { - client := NewClient(cc) - client.Use(interceptors...) - conn, err := client.Dial(context.Background(), _testAddr) - if err != nil { - panic(fmt.Errorf("did not connect: %v,req: %v %v", err, name, age)) - } - defer conn.Close() - c := pb.NewGreeterClient(conn) - resp, err = c.SayHello(ctx, &pb.HelloRequest{Name: name, Age: age}) - return -} - -func TestMain(t *testing.T) { - log.Init(nil) -} - -func Test_Warden(t *testing.T) { - xtrace.Init(&xtrace.Config{Addr: "127.0.0.1:9982", Timeout: xtime.Duration(time.Second * 3)}) - go _testOnce.Do(runServer(t)) - go runClient(context.Background(), &clientConfig, t, "trace_test", 0) - //testTrace(t, 9982, false) - //testInterceptorChain(t) - testValidation(t) - testServerRecovery(t) - testClientRecovery(t) - testTimeoutOpt(t) - testErrorDetail(t) - testECodeStatus(t) - testColorPass(t) - testRemotePort(t) - testLinkTimeout(t) - testClientConfig(t) - testBreaker(t) - testAllErrorCase(t) - testGracefulShutDown(t) -} - -func testValidation(t *testing.T) { - _, err := runClient(context.Background(), &clientConfig, t, "", 0) - if !ecode.EqualError(ecode.RequestErr, err) { - t.Fatalf("testValidation should return ecode.RequestErr,but is %v", err) - } -} - -func testTimeoutOpt(t *testing.T) { - ctx, cancel := context.WithTimeout(context.Background(), time.Millisecond*100) - defer cancel() - client := NewClient(&clientConfig) - conn, err := client.Dial(ctx, _testAddr) - if err != nil { - t.Fatalf("did not connect: %v", err) - } - defer conn.Close() - c := pb.NewGreeterClient(conn) - start := time.Now() - _, err = c.SayHello(ctx, &pb.HelloRequest{Name: "time_opt", Age: 0}, WithTimeoutCallOption(time.Millisecond*500)) - if err == nil { - t.Fatalf("recovery must return error") - } - if time.Since(start) < time.Millisecond*400 { - t.Fatalf("client timeout must be greater than 400 Milliseconds;err:=%v", err) - } -} - -func testAllErrorCase(t *testing.T) { - // } else if in.Name == "general_error" { - // return nil, fmt.Errorf("haha is error") - // } else if in.Name == "ecode_code_error" { - // return nil, ecode.CreativeArticleTagErr - // } else if in.Name == "pb_error_error" { - // return nil, &errpb.Error{ErrCode: 11122, ErrMessage: "haha"} - // } else if in.Name == "ecode_status_error" { - // return nil, ecode.Error(ecode.RequestErr, "RequestErr") - // } - ctx := context.Background() - t.Run("general_error", func(t *testing.T) { - _, err := runClient(ctx, &clientConfig, t, "general_error", 0) - assert.Contains(t, err.Error(), "haha") - ec := ecode.Cause(err) - assert.Equal(t, -500, ec.Code()) - // remove this assert in future - assert.Equal(t, "-500", ec.Message()) - }) - t.Run("ecode_code_error", func(t *testing.T) { - _, err := runClient(ctx, &clientConfig, t, "ecode_code_error", 0) - ec := ecode.Cause(err) - assert.Equal(t, ecode.Conflict.Code(), ec.Code()) - // remove this assert in future - assert.Equal(t, "-409", ec.Message()) - }) - t.Run("pb_error_error", func(t *testing.T) { - _, err := runClient(ctx, &clientConfig, t, "pb_error_error", 0) - ec := ecode.Cause(err) - assert.Equal(t, 11122, ec.Code()) - assert.Equal(t, "haha", ec.Message()) - }) - t.Run("ecode_status_error", func(t *testing.T) { - _, err := runClient(ctx, &clientConfig, t, "ecode_status_error", 0) - ec := ecode.Cause(err) - assert.Equal(t, ecode.RequestErr.Code(), ec.Code()) - assert.Equal(t, "RequestErr", ec.Message()) - }) -} - -func testBreaker(t *testing.T) { - client := NewClient(&clientConfig) - conn, err := client.Dial(context.Background(), _testAddr) - if err != nil { - t.Fatalf("did not connect: %v", err) - } - defer conn.Close() - c := pb.NewGreeterClient(conn) - for i := 0; i < 1000; i++ { - _, err := c.SayHello(context.Background(), &pb.HelloRequest{Name: "breaker_test"}) - if err != nil { - if ecode.EqualError(ecode.ServiceUnavailable, err) { - return - } - } - } - t.Fatalf("testBreaker failed!No breaker was triggered") -} - -func testColorPass(t *testing.T) { - ctx := nmd.NewContext(context.Background(), nmd.MD{ - nmd.Color: "red", - }) - resp, err := runClient(ctx, &clientConfig, t, "color_test", 0) - if err != nil { - t.Fatalf("testColorPass return error %v", err) - } - if resp == nil || resp.Message != "Hello red" { - t.Fatalf("testColorPass resp.Message must be red,%v", *resp) - } -} - -func testRemotePort(t *testing.T) { - ctx := nmd.NewContext(context.Background(), nmd.MD{ - nmd.RemotePort: "8000", - }) - _, err := runClient(ctx, &clientConfig, t, "test_remote_port", 8000) - if err != nil { - t.Fatalf("testRemotePort return error %v", err) - } -} - -func testLinkTimeout(t *testing.T) { - ctx, cancel := context.WithTimeout(context.Background(), time.Millisecond*200) - defer cancel() - _, err := runClient(ctx, &clientConfig, t, "timeout_test", 0) - if err == nil { - t.Fatalf("testLinkTimeout must return error") - } - if !ecode.EqualError(ecode.Deadline, err) { - t.Fatalf("testLinkTimeout must return error RPCDeadline,err:%v", err) - } -} -func testClientConfig(t *testing.T) { - _, err := runClient(context.Background(), &clientConfig2, t, "timeout_test2", 0) - if err == nil { - t.Fatalf("testLinkTimeout must return error") - } - if !ecode.EqualError(ecode.Deadline, err) { - t.Fatalf("testLinkTimeout must return error RPCDeadline,err:%v", err) - } -} - -func testGracefulShutDown(t *testing.T) { - wg := sync.WaitGroup{} - for i := 0; i < 10; i++ { - wg.Add(1) - go func() { - defer wg.Done() - resp, err := runClient(context.Background(), &clientConfig, t, "graceful_shutdown", 0) - if err != nil { - panic(fmt.Errorf("run graceful_shutdown client return(%v)", err)) - } - if !resp.Success || resp.Message != "Hello graceful_shutdown" { - panic(fmt.Errorf("run graceful_shutdown client return(%v,%v)", err, *resp)) - } - }() - } - go func() { - time.Sleep(time.Second) - ctx, cancel := context.WithTimeout(context.Background(), time.Second*3) - defer cancel() - server.Shutdown(ctx) - }() - wg.Wait() -} - -func testClientRecovery(t *testing.T) { - ctx := context.Background() - client := NewClient(&clientConfig) - client.Use(func(ctx context.Context, method string, req, reply interface{}, cc *grpc.ClientConn, invoker grpc.UnaryInvoker, opts ...grpc.CallOption) (ret error) { - invoker(ctx, method, req, reply, cc, opts...) - panic("client recovery test") - }) - - conn, err := client.Dial(ctx, _testAddr) - if err != nil { - t.Fatalf("did not connect: %v", err) - } - defer conn.Close() - c := pb.NewGreeterClient(conn) - - _, err = c.SayHello(ctx, &pb.HelloRequest{Name: "other_test", Age: 0}) - if err == nil { - t.Fatalf("recovery must return error") - } - e, ok := errors.Cause(err).(ecode.Codes) - if !ok { - t.Fatalf("recovery must return ecode error") - } - - if !ecode.EqualError(ecode.ServerErr, e) { - t.Fatalf("recovery must return ecode.RPCClientErr") - } -} - -func testServerRecovery(t *testing.T) { - ctx := context.Background() - client := NewClient(&clientConfig) - - conn, err := client.Dial(ctx, _testAddr) - if err != nil { - t.Fatalf("did not connect: %v", err) - } - defer conn.Close() - c := pb.NewGreeterClient(conn) - - _, err = c.SayHello(ctx, &pb.HelloRequest{Name: "recovery_test", Age: 0}) - if err == nil { - t.Fatalf("recovery must return error") - } - e, ok := errors.Cause(err).(ecode.Codes) - if !ok { - t.Fatalf("recovery must return ecode error") - } - - if e.Code() != ecode.ServerErr.Code() { - t.Fatalf("recovery must return ecode.ServerErr") - } -} - -func testInterceptorChain(t *testing.T) { - // NOTE: don't delete this sleep - time.Sleep(time.Millisecond) - if outPut[0] != "1" || outPut[1] != "3" || outPut[2] != "1" || outPut[3] != "3" || outPut[4] != "4" || outPut[5] != "2" || outPut[6] != "4" || outPut[7] != "2" { - t.Fatalf("outPut shoud be [1 3 1 3 4 2 4 2]!") - } -} - -func testErrorDetail(t *testing.T) { - _, err := runClient(context.Background(), &clientConfig2, t, "error_detail", 0) - if err == nil { - t.Fatalf("testErrorDetail must return error") - } - if ec, ok := errors.Cause(err).(ecode.Codes); !ok { - t.Fatalf("testErrorDetail must return ecode error") - } else if ec.Code() != 123456 || ec.Message() != "test_error_detail" || len(ec.Details()) == 0 { - t.Fatalf("testErrorDetail must return code:123456 and message:test_error_detail, code: %d, message: %s, details length: %d", ec.Code(), ec.Message(), len(ec.Details())) - } else if _, ok := ec.Details()[len(ec.Details())-1].(*pb.HelloReply); !ok { - t.Fatalf("expect get pb.HelloReply %#v", ec.Details()[len(ec.Details())-1]) - } -} - -func testECodeStatus(t *testing.T) { - _, err := runClient(context.Background(), &clientConfig2, t, "ecode_status", 0) - if err == nil { - t.Fatalf("testECodeStatus must return error") - } - st, ok := errors.Cause(err).(*ecode.Status) - if !ok { - t.Fatalf("testECodeStatus must return *ecode.Status") - } - if st.Code() != int(ecode.RequestErr) && st.Message() != "RequestErr" { - t.Fatalf("testECodeStatus must return code: -400, message: RequestErr get: code: %d, message: %s", st.Code(), st.Message()) - } - detail := st.Details()[0].(*pb.HelloReply) - if !detail.Success || detail.Message != "status" { - t.Fatalf("wrong detail") - } -} - -func testTrace(t *testing.T, port int, isStream bool) { - listener, err := net.ListenUDP("udp", &net.UDPAddr{IP: net.ParseIP("127.0.0.1"), Port: port}) - if err != nil { - t.Fatalf("listent udp failed, %v", err) - return - } - data := make([]byte, 1024) - strs := make([][]string, 0) - for { - var n int - n, _, err = listener.ReadFromUDP(data) - if err != nil { - t.Fatalf("read from udp faild, %v", err) - } - str := strings.Split(string(data[:n]), _separator) - strs = append(strs, str) - - if len(strs) == 2 { - break - } - } - if len(strs[0]) == 0 || len(strs[1]) == 0 { - t.Fatalf("trace str's length must be greater than 0") - } -} - -func BenchmarkServer(b *testing.B) { - server := NewServer(&ServerConfig{Addr: _testAddr, Timeout: xtime.Duration(time.Second)}) - go func() { - pb.RegisterGreeterServer(server.Server(), &helloServer{}) - if _, err := server.Start(); err != nil { - os.Exit(0) - return - } - }() - defer func() { - server.Server().Stop() - }() - client := NewClient(&clientConfig) - conn, err := client.Dial(context.Background(), _testAddr) - if err != nil { - conn.Close() - b.Fatalf("did not connect: %v", err) - } - b.ResetTimer() - b.RunParallel(func(parab *testing.PB) { - for parab.Next() { - c := pb.NewGreeterClient(conn) - resp, err := c.SayHello(context.Background(), &pb.HelloRequest{Name: "benchmark_test", Age: 1}) - if err != nil { - conn.Close() - b.Fatalf("c.SayHello failed: %v,req: %v %v", err, "benchmark", 1) - } - if !resp.Success { - b.Error("response not success!") - } - } - }) - conn.Close() -} - -func TestParseDSN(t *testing.T) { - dsn := "tcp://0.0.0.0:80/?timeout=100ms&idleTimeout=120s&keepaliveInterval=120s&keepaliveTimeout=20s&maxLife=4h&closeWait=3s" - config := parseDSN(dsn) - if config.Network != "tcp" || config.Addr != "0.0.0.0:80" || time.Duration(config.Timeout) != time.Millisecond*100 || - time.Duration(config.IdleTimeout) != time.Second*120 || time.Duration(config.KeepAliveInterval) != time.Second*120 || - time.Duration(config.MaxLifeTime) != time.Hour*4 || time.Duration(config.ForceCloseWait) != time.Second*3 || time.Duration(config.KeepAliveTimeout) != time.Second*20 { - t.Fatalf("parseDSN(%s) not compare config result(%+v)", dsn, config) - } - - dsn = "unix:///temp/warden.sock?timeout=300ms" - config = parseDSN(dsn) - if config.Network != "unix" || config.Addr != "/temp/warden.sock" || time.Duration(config.Timeout) != time.Millisecond*300 { - t.Fatalf("parseDSN(%s) not compare config result(%+v)", dsn, config) - } -} - -type testServer struct { - helloFn func(ctx context.Context, req *pb.HelloRequest) (*pb.HelloReply, error) -} - -func (t *testServer) SayHello(ctx context.Context, req *pb.HelloRequest) (*pb.HelloReply, error) { - return t.helloFn(ctx, req) -} - -func (t *testServer) StreamHello(pb.Greeter_StreamHelloServer) error { panic("not implemented") } - -// NewTestServerClient . -func NewTestServerClient(invoker func(ctx context.Context, req *pb.HelloRequest) (*pb.HelloReply, error), svrcfg *ServerConfig, clicfg *ClientConfig) (pb.GreeterClient, func() error) { - srv := NewServer(svrcfg) - pb.RegisterGreeterServer(srv.Server(), &testServer{helloFn: invoker}) - - lis, err := net.Listen("tcp", "127.0.0.1:0") - if err != nil { - panic(err) - } - ch := make(chan bool, 1) - go func() { - ch <- true - srv.Serve(lis) - }() - <-ch - println(lis.Addr().String()) - conn, err := NewConn(lis.Addr().String()) - if err != nil { - panic(err) - } - return pb.NewGreeterClient(conn), func() error { return srv.Shutdown(context.Background()) } -} - -func TestMetadata(t *testing.T) { - cli, cancel := NewTestServerClient(func(ctx context.Context, req *pb.HelloRequest) (*pb.HelloReply, error) { - assert.Equal(t, "red", nmd.String(ctx, nmd.Color)) - assert.Equal(t, "2.2.3.3", nmd.String(ctx, nmd.RemoteIP)) - assert.Equal(t, "2233", nmd.String(ctx, nmd.RemotePort)) - return &pb.HelloReply{}, nil - }, nil, nil) - defer cancel() - - ctx := nmd.NewContext(context.Background(), nmd.MD{ - nmd.Color: "red", - nmd.RemoteIP: "2.2.3.3", - nmd.RemotePort: "2233", - }) - _, err := cli.SayHello(ctx, &pb.HelloRequest{Name: "test"}) - assert.Nil(t, err) -} - -func TestStartWithAddr(t *testing.T) { - configuredAddr := "127.0.0.1:0" - server = NewServer(&ServerConfig{Addr: configuredAddr, Timeout: xtime.Duration(time.Second)}) - if _, realAddr, err := server.StartWithAddr(); err == nil && realAddr != nil { - assert.NotEqual(t, realAddr.String(), configuredAddr) - } else { - assert.NotNil(t, realAddr) - assert.Nil(t, err) - } -} diff --git a/pkg/net/rpc/warden/stats.go b/pkg/net/rpc/warden/stats.go deleted file mode 100644 index 924ba51f5..000000000 --- a/pkg/net/rpc/warden/stats.go +++ /dev/null @@ -1,25 +0,0 @@ -package warden - -import ( - "context" - "strconv" - - nmd "github.com/go-kratos/kratos/pkg/net/rpc/warden/internal/metadata" - "github.com/go-kratos/kratos/pkg/stat/sys/cpu" - - "google.golang.org/grpc" - gmd "google.golang.org/grpc/metadata" -) - -func (s *Server) stats() grpc.UnaryServerInterceptor { - return func(ctx context.Context, req interface{}, args *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (resp interface{}, err error) { - resp, err = handler(ctx, req) - var cpustat cpu.Stat - cpu.ReadStat(&cpustat) - if cpustat.Usage != 0 { - trailer := gmd.Pairs([]string{nmd.CPUUsage, strconv.FormatInt(int64(cpustat.Usage), 10)}...) - grpc.SetTrailer(ctx, trailer) - } - return - } -} diff --git a/pkg/net/rpc/warden/validate.go b/pkg/net/rpc/warden/validate.go deleted file mode 100644 index 706123b40..000000000 --- a/pkg/net/rpc/warden/validate.go +++ /dev/null @@ -1,37 +0,0 @@ -package warden - -import ( - "context" - - "google.golang.org/grpc/codes" - "google.golang.org/grpc/status" - - "google.golang.org/grpc" - "gopkg.in/go-playground/validator.v9" -) - -var validate = validator.New() - -// Validate return a client interceptor validate incoming request per RPC call. -func (s *Server) validate() grpc.UnaryServerInterceptor { - return func(ctx context.Context, req interface{}, args *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (resp interface{}, err error) { - if err = validate.Struct(req); err != nil { - err = status.Error(codes.InvalidArgument, err.Error()) - return - } - resp, err = handler(ctx, req) - return - } -} - -// RegisterValidation adds a validation Func to a Validate's map of validators denoted by the key -// NOTE: if the key already exists, the previous validation function will be replaced. -// NOTE: this method is not thread-safe it is intended that these all be registered prior to any validation -func (s *Server) RegisterValidation(key string, fn validator.Func) error { - return validate.RegisterValidation(key, fn) -} - -// GetValidate return the default validate -func (s *Server) GetValidate() *validator.Validate { - return validate -} diff --git a/pkg/net/trace/README.md b/pkg/net/trace/README.md deleted file mode 100644 index ce87fa101..000000000 --- a/pkg/net/trace/README.md +++ /dev/null @@ -1,20 +0,0 @@ -# net/trace - -## 项目简介 -1. 提供Trace的接口规范 -2. 提供 trace 对Tracer接口的实现,供业务接入使用 - -## 接入示例 -1. 启动接入示例 - ```go - trace.Init(traceConfig) // traceConfig is Config object with value. - ``` -2. 配置参考 - ```toml - [tracer] - network = "unixgram" - addr = "/var/run/dapper-collect/dapper-collect.sock" - ``` - -## 测试 -1. 执行当前目录下所有测试文件,测试所有功能 diff --git a/pkg/net/trace/config.go b/pkg/net/trace/config.go deleted file mode 100644 index 3f46cf9cc..000000000 --- a/pkg/net/trace/config.go +++ /dev/null @@ -1,75 +0,0 @@ -package trace - -import ( - "flag" - "fmt" - "os" - "time" - - "github.com/pkg/errors" - - "github.com/go-kratos/kratos/pkg/conf/dsn" - "github.com/go-kratos/kratos/pkg/conf/env" - xtime "github.com/go-kratos/kratos/pkg/time" -) - -var _traceDSN = "unixgram:///var/run/dapper-collect/dapper-collect.sock" - -func init() { - if v := os.Getenv("TRACE"); v != "" { - _traceDSN = v - } - flag.StringVar(&_traceDSN, "trace", _traceDSN, "trace report dsn, or use TRACE env.") -} - -// Config config. -type Config struct { - // Report network e.g. unixgram, tcp, udp - Network string `dsn:"network"` - // For TCP and UDP networks, the addr has the form "host:port". - // For Unix networks, the address must be a file system path. - Addr string `dsn:"address"` - // Report timeout - Timeout xtime.Duration `dsn:"query.timeout,200ms"` - // DisableSample - DisableSample bool `dsn:"query.disable_sample"` - // ProtocolVersion - ProtocolVersion int32 `dsn:"query.protocol_version,1"` - // Probability probability sampling - Probability float32 `dsn:"-"` -} - -func parseDSN(rawdsn string) (*Config, error) { - d, err := dsn.Parse(rawdsn) - if err != nil { - return nil, errors.Wrapf(err, "trace: invalid dsn: %s", rawdsn) - } - cfg := new(Config) - if _, err = d.Bind(cfg); err != nil { - return nil, errors.Wrapf(err, "trace: invalid dsn: %s", rawdsn) - } - return cfg, nil -} - -// TracerFromEnvFlag new tracer from env and flag -func TracerFromEnvFlag() (Tracer, error) { - cfg, err := parseDSN(_traceDSN) - if err != nil { - return nil, err - } - report := newReport(cfg.Network, cfg.Addr, time.Duration(cfg.Timeout), cfg.ProtocolVersion) - return NewTracer(env.AppID, report, cfg.DisableSample), nil -} - -// Init init trace report. -func Init(cfg *Config) { - if cfg == nil { - // paser config from env - var err error - if cfg, err = parseDSN(_traceDSN); err != nil { - panic(fmt.Errorf("parse trace dsn error: %s", err)) - } - } - report := newReport(cfg.Network, cfg.Addr, time.Duration(cfg.Timeout), cfg.ProtocolVersion) - SetGlobalTracer(NewTracer(env.AppID, report, cfg.DisableSample)) -} diff --git a/pkg/net/trace/config_test.go b/pkg/net/trace/config_test.go deleted file mode 100644 index 1d7726806..000000000 --- a/pkg/net/trace/config_test.go +++ /dev/null @@ -1,33 +0,0 @@ -package trace - -import ( - "testing" - - "github.com/stretchr/testify/assert" -) - -func TestParseDSN(t *testing.T) { - _, err := parseDSN(_traceDSN) - if err != nil { - t.Error(err) - } -} - -func TestTraceFromEnvFlag(t *testing.T) { - _, err := TracerFromEnvFlag() - if err != nil { - t.Error(err) - } -} - -func TestInit(t *testing.T) { - Init(nil) - _, ok := _tracer.(nooptracer) - assert.False(t, ok) - - _tracer = nooptracer{} - - Init(&Config{Network: "unixgram", Addr: "unixgram:///var/run/dapper-collect/dapper-collect.sock"}) - _, ok = _tracer.(nooptracer) - assert.False(t, ok) -} diff --git a/pkg/net/trace/const.go b/pkg/net/trace/const.go deleted file mode 100644 index 616b0fe08..000000000 --- a/pkg/net/trace/const.go +++ /dev/null @@ -1,7 +0,0 @@ -package trace - -// Trace key -const ( - KratosTraceID = "kratos-trace-id" - KratosTraceDebug = "kratos-trace-debug" -) diff --git a/pkg/net/trace/context.go b/pkg/net/trace/context.go deleted file mode 100644 index 50e0be170..000000000 --- a/pkg/net/trace/context.go +++ /dev/null @@ -1,110 +0,0 @@ -package trace - -import ( - "strconv" - "strings" - - "github.com/pkg/errors" -) - -const ( - flagSampled = 0x01 - flagDebug = 0x02 -) - -var ( - errEmptyTracerString = errors.New("trace: cannot convert empty string to spancontext") - errInvalidTracerString = errors.New("trace: string does not match spancontext string format") -) - -// SpanContext implements opentracing.SpanContext -type spanContext struct { - // TraceID represents globally unique ID of the trace. - // Usually generated as a random number. - TraceID uint64 - - // SpanID represents span ID that must be unique within its trace, - // but does not have to be globally unique. - SpanID uint64 - - // ParentID refers to the ID of the parent span. - // Should be 0 if the current span is a root span. - ParentID uint64 - - // Flags is a bitmap containing such bits as 'sampled' and 'debug'. - Flags byte - - // Probability - Probability float32 - - // Level current level - Level int -} - -func (c spanContext) isSampled() bool { - return (c.Flags & flagSampled) == flagSampled -} - -func (c spanContext) isDebug() bool { - return (c.Flags & flagDebug) == flagDebug -} - -// IsValid check spanContext valid -func (c spanContext) IsValid() bool { - return c.TraceID != 0 && c.SpanID != 0 -} - -// emptyContext emptyContext -var emptyContext = spanContext{} - -// String convert spanContext to String -// {TraceID}:{SpanID}:{ParentID}:{flags}:[extend...] -// TraceID: uint64 base16 -// SpanID: uint64 base16 -// ParentID: uint64 base16 -// flags: -// - :0 sampled flag -// - :1 debug flag -// extend: -// sample-rate: s-{base16(BigEndian(float32))} -func (c spanContext) String() string { - base := make([]string, 4) - base[0] = strconv.FormatUint(c.TraceID, 16) - base[1] = strconv.FormatUint(c.SpanID, 16) - base[2] = strconv.FormatUint(c.ParentID, 16) - base[3] = strconv.FormatUint(uint64(c.Flags), 16) - return strings.Join(base, ":") -} - -// ContextFromString parse spanContext form string -func contextFromString(value string) (spanContext, error) { - if value == "" { - return emptyContext, errEmptyTracerString - } - items := strings.Split(value, ":") - if len(items) < 4 { - return emptyContext, errInvalidTracerString - } - parseHexUint64 := func(hexs []string) ([]uint64, error) { - rets := make([]uint64, len(hexs)) - var err error - for i, hex := range hexs { - rets[i], err = strconv.ParseUint(hex, 16, 64) - if err != nil { - break - } - } - return rets, err - } - rets, err := parseHexUint64(items[0:4]) - if err != nil { - return emptyContext, errInvalidTracerString - } - sctx := spanContext{ - TraceID: rets[0], - SpanID: rets[1], - ParentID: rets[2], - Flags: byte(rets[3]), - } - return sctx, nil -} diff --git a/pkg/net/trace/context_test.go b/pkg/net/trace/context_test.go deleted file mode 100644 index fb5e02f92..000000000 --- a/pkg/net/trace/context_test.go +++ /dev/null @@ -1,26 +0,0 @@ -package trace - -import ( - "testing" -) - -func TestSpanContext(t *testing.T) { - pctx := &spanContext{ - ParentID: genID(), - SpanID: genID(), - TraceID: genID(), - Flags: flagSampled, - } - if !pctx.isSampled() { - t.Error("expect sampled") - } - value := pctx.String() - t.Logf("bili-trace-id: %s", value) - pctx2, err := contextFromString(value) - if err != nil { - t.Error(err) - } - if pctx2.ParentID != pctx.ParentID || pctx2.SpanID != pctx.SpanID || pctx2.TraceID != pctx.TraceID || pctx2.Flags != pctx.Flags { - t.Errorf("wrong spancontext get %+v -> %+v", pctx, pctx2) - } -} diff --git a/pkg/net/trace/dapper.go b/pkg/net/trace/dapper.go deleted file mode 100644 index 88934b103..000000000 --- a/pkg/net/trace/dapper.go +++ /dev/null @@ -1,189 +0,0 @@ -package trace - -import ( - "log" - "os" - "sync" - "time" -) - -const ( - _maxLevel = 64 - // hard code reset probability at 0.00025, 1/4000 - _probability = 0.00025 -) - -// NewTracer new a tracer. -func NewTracer(serviceName string, report reporter, disableSample bool) Tracer { - sampler := newSampler(_probability) - - // default internal tags - tags := extendTag() - stdlog := log.New(os.Stderr, "trace", log.LstdFlags) - return &dapper{ - serviceName: serviceName, - disableSample: disableSample, - propagators: map[interface{}]propagator{ - HTTPFormat: httpPropagator{}, - GRPCFormat: grpcPropagator{}, - }, - reporter: report, - sampler: sampler, - tags: tags, - pool: &sync.Pool{New: func() interface{} { return new(Span) }}, - stdlog: stdlog, - } -} - -type dapper struct { - serviceName string - disableSample bool - tags []Tag - reporter reporter - propagators map[interface{}]propagator - pool *sync.Pool - stdlog *log.Logger - sampler sampler -} - -func (d *dapper) New(operationName string, opts ...Option) Trace { - opt := defaultOption - for _, fn := range opts { - fn(&opt) - } - traceID := genID() - var sampled bool - var probability float32 - if d.disableSample { - sampled = true - probability = 1 - } else { - sampled, probability = d.sampler.IsSampled(traceID, operationName) - } - pctx := spanContext{TraceID: traceID} - if sampled { - pctx.Flags = flagSampled - pctx.Probability = probability - } - if opt.Debug { - pctx.Flags |= flagDebug - return d.newSpanWithContext(operationName, pctx).SetTag(TagString(TagSpanKind, "server")).SetTag(TagBool("debug", true)) - } - // 为了兼容临时为 New 的 Span 设置 span.kind - return d.newSpanWithContext(operationName, pctx).SetTag(TagString(TagSpanKind, "server")) -} - -func (d *dapper) newSpanWithContext(operationName string, pctx spanContext) Trace { - sp := d.getSpan() - // is span is not sampled just return a span with this context, no need clear it - //if !pctx.isSampled() { - // sp.context = pctx - // return sp - //} - if pctx.Level > _maxLevel { - // if span reach max limit level return noopspan - return noopspan{} - } - level := pctx.Level + 1 - nctx := spanContext{ - TraceID: pctx.TraceID, - ParentID: pctx.SpanID, - Flags: pctx.Flags, - Level: level, - } - if pctx.SpanID == 0 { - nctx.SpanID = pctx.TraceID - } else { - nctx.SpanID = genID() - } - sp.operationName = operationName - sp.context = nctx - sp.startTime = time.Now() - sp.tags = append(sp.tags, d.tags...) - return sp -} - -func (d *dapper) Inject(t Trace, format interface{}, carrier interface{}) error { - // if carrier implement Carrier use direct, ignore format - carr, ok := carrier.(Carrier) - if ok { - t.Visit(carr.Set) - return nil - } - // use Built-in propagators - pp, ok := d.propagators[format] - if !ok { - return ErrUnsupportedFormat - } - carr, err := pp.Inject(carrier) - if err != nil { - return err - } - if t != nil { - t.Visit(carr.Set) - } - return nil -} - -func (d *dapper) Extract(format interface{}, carrier interface{}) (Trace, error) { - sp, err := d.extract(format, carrier) - if err != nil { - return sp, err - } - // 为了兼容临时为 New 的 Span 设置 span.kind - return sp.SetTag(TagString(TagSpanKind, "server")), nil -} - -func (d *dapper) extract(format interface{}, carrier interface{}) (Trace, error) { - // if carrier implement Carrier use direct, ignore format - carr, ok := carrier.(Carrier) - if !ok { - // use Built-in propagators - pp, ok := d.propagators[format] - if !ok { - return nil, ErrUnsupportedFormat - } - var err error - if carr, err = pp.Extract(carrier); err != nil { - return nil, err - } - } - pctx, err := contextFromString(carr.Get(KratosTraceID)) - if err != nil { - return nil, err - } - // NOTE: call SetTitle after extract trace - return d.newSpanWithContext("", pctx), nil -} - -func (d *dapper) Close() error { - return d.reporter.Close() -} - -func (d *dapper) report(sp *Span) { - if sp.context.isSampled() { - if err := d.reporter.WriteSpan(sp); err != nil { - d.stdlog.Printf("marshal trace span error: %s", err) - } - } - d.putSpan(sp) -} - -func (d *dapper) putSpan(sp *Span) { - if len(sp.tags) > 32 { - sp.tags = nil - } - if len(sp.logs) > 32 { - sp.logs = nil - } - d.pool.Put(sp) -} - -func (d *dapper) getSpan() *Span { - sp := d.pool.Get().(*Span) - sp.dapper = d - sp.childs = 0 - sp.tags = sp.tags[:0] - sp.logs = sp.logs[:0] - return sp -} diff --git a/pkg/net/trace/dapper_test.go b/pkg/net/trace/dapper_test.go deleted file mode 100644 index 0ee9cbbf0..000000000 --- a/pkg/net/trace/dapper_test.go +++ /dev/null @@ -1,136 +0,0 @@ -package trace - -import ( - "fmt" - "net/http" - "testing" - - "github.com/stretchr/testify/assert" - "google.golang.org/grpc/metadata" -) - -type mockReport struct { - sps []*Span -} - -func (m *mockReport) WriteSpan(sp *Span) error { - m.sps = append(m.sps, sp) - return nil -} - -func (m *mockReport) Close() error { - return nil -} - -func TestDapperPropagation(t *testing.T) { - t.Run("test HTTP progagation", func(t *testing.T) { - report := &mockReport{} - t1 := NewTracer("service1", report, true) - t2 := NewTracer("service2", report, true) - sp1 := t1.New("opt_1") - sp2 := sp1.Fork("", "opt_client") - header := make(http.Header) - t1.Inject(sp2, HTTPFormat, header) - sp3, err := t2.Extract(HTTPFormat, header) - if err != nil { - t.Fatal(err) - } - sp3.Finish(nil) - sp2.Finish(nil) - sp1.Finish(nil) - - assert.Len(t, report.sps, 3) - assert.Equal(t, report.sps[2].context.ParentID, uint64(0)) - assert.Equal(t, report.sps[0].context.TraceID, report.sps[1].context.TraceID) - assert.Equal(t, report.sps[2].context.TraceID, report.sps[1].context.TraceID) - - assert.Equal(t, report.sps[1].context.ParentID, report.sps[2].context.SpanID) - assert.Equal(t, report.sps[0].context.ParentID, report.sps[1].context.SpanID) - }) - t.Run("test gRPC progagation", func(t *testing.T) { - report := &mockReport{} - t1 := NewTracer("service1", report, true) - t2 := NewTracer("service2", report, true) - sp1 := t1.New("opt_1") - sp2 := sp1.Fork("", "opt_client") - md := make(metadata.MD) - t1.Inject(sp2, GRPCFormat, md) - sp3, err := t2.Extract(GRPCFormat, md) - if err != nil { - t.Fatal(err) - } - sp3.Finish(nil) - sp2.Finish(nil) - sp1.Finish(nil) - - assert.Len(t, report.sps, 3) - assert.Equal(t, report.sps[2].context.ParentID, uint64(0)) - assert.Equal(t, report.sps[0].context.TraceID, report.sps[1].context.TraceID) - assert.Equal(t, report.sps[2].context.TraceID, report.sps[1].context.TraceID) - - assert.Equal(t, report.sps[1].context.ParentID, report.sps[2].context.SpanID) - assert.Equal(t, report.sps[0].context.ParentID, report.sps[1].context.SpanID) - }) - t.Run("test normal", func(t *testing.T) { - report := &mockReport{} - t1 := NewTracer("service1", report, true) - sp1 := t1.New("test123") - sp1.Finish(nil) - }) - t.Run("test debug progagation", func(t *testing.T) { - report := &mockReport{} - t1 := NewTracer("service1", report, true) - t2 := NewTracer("service2", report, true) - sp1 := t1.New("opt_1", EnableDebug()) - sp2 := sp1.Fork("", "opt_client") - header := make(http.Header) - t1.Inject(sp2, HTTPFormat, header) - sp3, err := t2.Extract(HTTPFormat, header) - if err != nil { - t.Fatal(err) - } - sp3.Finish(nil) - sp2.Finish(nil) - sp1.Finish(nil) - - assert.Len(t, report.sps, 3) - assert.Equal(t, report.sps[2].context.ParentID, uint64(0)) - assert.Equal(t, report.sps[0].context.TraceID, report.sps[1].context.TraceID) - assert.Equal(t, report.sps[2].context.TraceID, report.sps[1].context.TraceID) - - assert.Equal(t, report.sps[1].context.ParentID, report.sps[2].context.SpanID) - assert.Equal(t, report.sps[0].context.ParentID, report.sps[1].context.SpanID) - }) -} - -func BenchmarkSample(b *testing.B) { - err := fmt.Errorf("test error") - report := &mockReport{} - t1 := NewTracer("service1", report, true) - for i := 0; i < b.N; i++ { - sp1 := t1.New("test_opt1") - sp1.SetTag(TagString("test", "123")) - sp2 := sp1.Fork("", "opt2") - sp3 := sp2.Fork("", "opt3") - sp3.SetTag(TagString("test", "123")) - sp3.Finish(nil) - sp2.Finish(&err) - sp1.Finish(nil) - } -} - -func BenchmarkDisableSample(b *testing.B) { - err := fmt.Errorf("test error") - report := &mockReport{} - t1 := NewTracer("service1", report, true) - for i := 0; i < b.N; i++ { - sp1 := t1.New("test_opt1") - sp1.SetTag(TagString("test", "123")) - sp2 := sp1.Fork("", "opt2") - sp3 := sp2.Fork("", "opt3") - sp3.SetTag(TagString("test", "123")) - sp3.Finish(nil) - sp2.Finish(&err) - sp1.Finish(nil) - } -} diff --git a/pkg/net/trace/jaeger/config.go b/pkg/net/trace/jaeger/config.go deleted file mode 100644 index e5170cc25..000000000 --- a/pkg/net/trace/jaeger/config.go +++ /dev/null @@ -1,33 +0,0 @@ -package jaeger - -import ( - "flag" - "os" - - "github.com/go-kratos/kratos/pkg/conf/env" - "github.com/go-kratos/kratos/pkg/net/trace" -) - -var ( - _jaegerAppID = env.AppID - _jaegerEndpoint = "http://127.0.0.1:9191" -) - -func init() { - if v := os.Getenv("JAEGER_ENDPOINT"); v != "" { - _jaegerEndpoint = v - } - - if v := os.Getenv("JAEGER_APPID"); v != "" { - _jaegerAppID = v - } - - flag.StringVar(&_jaegerEndpoint, "jaeger_endpoint", _jaegerEndpoint, "jaeger report endpoint, or use JAEGER_ENDPOINT env.") - flag.StringVar(&_jaegerAppID, "jaeger_appid", _jaegerAppID, "jaeger report appid, or use JAEGER_APPID env.") -} - -// Init Init -func Init() { - c := &Config{Endpoint: _jaegerEndpoint, BatchSize: 120} - trace.SetGlobalTracer(trace.NewTracer(_jaegerAppID, newReport(c), true)) -} diff --git a/pkg/net/trace/jaeger/http_transport.go b/pkg/net/trace/jaeger/http_transport.go deleted file mode 100644 index 17ab38e2b..000000000 --- a/pkg/net/trace/jaeger/http_transport.go +++ /dev/null @@ -1,314 +0,0 @@ -package jaeger - -import ( - "bytes" - "fmt" - "io" - "io/ioutil" - "net/http" - "time" - - "github.com/opentracing/opentracing-go" - ja "github.com/uber/jaeger-client-go" - "github.com/uber/jaeger-client-go/thrift" - j "github.com/uber/jaeger-client-go/thrift-gen/jaeger" -) - -// Default timeout for http request in seconds -const defaultHTTPTimeout = time.Second * 5 - -// HTTPTransport implements Transport by forwarding spans to a http server. -type HTTPTransport struct { - url string - client *http.Client - batchSize int - spans []*j.Span - process *j.Process - httpCredentials *HTTPBasicAuthCredentials - headers map[string]string -} - -// HTTPBasicAuthCredentials stores credentials for HTTP basic auth. -type HTTPBasicAuthCredentials struct { - username string - password string -} - -// HTTPOption sets a parameter for the HttpCollector -type HTTPOption func(c *HTTPTransport) - -// HTTPTimeout sets maximum timeout for http request. -func HTTPTimeout(duration time.Duration) HTTPOption { - return func(c *HTTPTransport) { c.client.Timeout = duration } -} - -// HTTPBatchSize sets the maximum batch size, after which a collect will be -// triggered. The default batch size is 100 spans. -func HTTPBatchSize(n int) HTTPOption { - return func(c *HTTPTransport) { c.batchSize = n } -} - -// HTTPBasicAuth sets the credentials required to perform HTTP basic auth -func HTTPBasicAuth(username string, password string) HTTPOption { - return func(c *HTTPTransport) { - c.httpCredentials = &HTTPBasicAuthCredentials{username: username, password: password} - } -} - -// HTTPRoundTripper configures the underlying Transport on the *http.Client -// that is used -func HTTPRoundTripper(transport http.RoundTripper) HTTPOption { - return func(c *HTTPTransport) { - c.client.Transport = transport - } -} - -// HTTPHeaders defines the HTTP headers that will be attached to the jaeger client's HTTP request -func HTTPHeaders(headers map[string]string) HTTPOption { - return func(c *HTTPTransport) { - c.headers = headers - } -} - -// NewHTTPTransport returns a new HTTP-backend transport. url should be an http -// url of the collector to handle POST request, typically something like: -// http://hostname:14268/api/traces?format=jaeger.thrift -func NewHTTPTransport(url string, options ...HTTPOption) *HTTPTransport { - c := &HTTPTransport{ - url: url, - client: &http.Client{Timeout: defaultHTTPTimeout}, - batchSize: 100, - spans: []*j.Span{}, - } - - for _, option := range options { - option(c) - } - return c -} - -// Append implements Transport. -func (c *HTTPTransport) Append(span *Span) (int, error) { - if c.process == nil { - process := j.NewProcess() - process.ServiceName = span.ServiceName() - c.process = process - } - jSpan := BuildJaegerThrift(span) - c.spans = append(c.spans, jSpan) - if len(c.spans) >= c.batchSize { - return c.Flush() - } - return 0, nil -} - -// Flush implements Transport. -func (c *HTTPTransport) Flush() (int, error) { - count := len(c.spans) - if count == 0 { - return 0, nil - } - err := c.send(c.spans) - c.spans = c.spans[:0] - return count, err -} - -// Close implements Transport. -func (c *HTTPTransport) Close() error { - return nil -} - -func (c *HTTPTransport) send(spans []*j.Span) error { - batch := &j.Batch{ - Spans: spans, - Process: c.process, - } - body, err := serializeThrift(batch) - if err != nil { - return err - } - req, err := http.NewRequest("POST", c.url, body) - if err != nil { - return err - } - req.Header.Set("Content-Type", "application/x-thrift") - for k, v := range c.headers { - req.Header.Set(k, v) - } - - if c.httpCredentials != nil { - req.SetBasicAuth(c.httpCredentials.username, c.httpCredentials.password) - } - - resp, err := c.client.Do(req) - if err != nil { - return err - } - io.Copy(ioutil.Discard, resp.Body) - resp.Body.Close() - if resp.StatusCode >= http.StatusBadRequest { - return fmt.Errorf("error from collector: %d", resp.StatusCode) - } - return nil -} - -func serializeThrift(obj thrift.TStruct) (*bytes.Buffer, error) { - t := thrift.NewTMemoryBuffer() - p := thrift.NewTBinaryProtocolTransport(t) - if err := obj.Write(p); err != nil { - return nil, err - } - return t.Buffer, nil -} - -func BuildJaegerThrift(span *Span) *j.Span { - span.Lock() - defer span.Unlock() - startTime := span.startTime.UnixNano() / 1000 - duration := span.duration.Nanoseconds() / int64(time.Microsecond) - jaegerSpan := &j.Span{ - TraceIdLow: int64(span.context.traceID.Low), - TraceIdHigh: int64(span.context.traceID.High), - SpanId: int64(span.context.spanID), - ParentSpanId: int64(span.context.parentID), - OperationName: span.operationName, - Flags: int32(span.context.samplingState.flags()), - StartTime: startTime, - Duration: duration, - Tags: buildTags(span.tags, 100), - Logs: buildLogs(span.logs), - References: buildReferences(span.references), - } - return jaegerSpan -} - -func stringify(value interface{}) string { - if s, ok := value.(string); ok { - return s - } - return fmt.Sprintf("%+v", value) -} - -func truncateString(value string, maxLength int) string { - // we ignore the problem of utf8 runes possibly being sliced in the middle, - // as it is rather expensive to iterate through each tag just to find rune - // boundaries. - if len(value) > maxLength { - return value[:maxLength] - } - return value -} - -func buildTags(tags []Tag, maxTagValueLength int) []*j.Tag { - jTags := make([]*j.Tag, 0, len(tags)) - for _, tag := range tags { - jTag := buildTag(&tag, maxTagValueLength) - jTags = append(jTags, jTag) - } - return jTags -} -func buildTag(tag *Tag, maxTagValueLength int) *j.Tag { - jTag := &j.Tag{Key: tag.key} - switch value := tag.value.(type) { - case string: - vStr := truncateString(value, maxTagValueLength) - jTag.VStr = &vStr - jTag.VType = j.TagType_STRING - case []byte: - if len(value) > maxTagValueLength { - value = value[:maxTagValueLength] - } - jTag.VBinary = value - jTag.VType = j.TagType_BINARY - case int: - vLong := int64(value) - jTag.VLong = &vLong - jTag.VType = j.TagType_LONG - case uint: - vLong := int64(value) - jTag.VLong = &vLong - jTag.VType = j.TagType_LONG - case int8: - vLong := int64(value) - jTag.VLong = &vLong - jTag.VType = j.TagType_LONG - case uint8: - vLong := int64(value) - jTag.VLong = &vLong - jTag.VType = j.TagType_LONG - case int16: - vLong := int64(value) - jTag.VLong = &vLong - jTag.VType = j.TagType_LONG - case uint16: - vLong := int64(value) - jTag.VLong = &vLong - jTag.VType = j.TagType_LONG - case int32: - vLong := int64(value) - jTag.VLong = &vLong - jTag.VType = j.TagType_LONG - case uint32: - vLong := int64(value) - jTag.VLong = &vLong - jTag.VType = j.TagType_LONG - case int64: - vLong := value - jTag.VLong = &vLong - jTag.VType = j.TagType_LONG - case uint64: - vLong := int64(value) - jTag.VLong = &vLong - jTag.VType = j.TagType_LONG - case float32: - vDouble := float64(value) - jTag.VDouble = &vDouble - jTag.VType = j.TagType_DOUBLE - case float64: - vDouble := value - jTag.VDouble = &vDouble - jTag.VType = j.TagType_DOUBLE - case bool: - vBool := value - jTag.VBool = &vBool - jTag.VType = j.TagType_BOOL - default: - vStr := truncateString(stringify(value), maxTagValueLength) - jTag.VStr = &vStr - jTag.VType = j.TagType_STRING - } - return jTag -} - -func buildLogs(logs []opentracing.LogRecord) []*j.Log { - jLogs := make([]*j.Log, 0, len(logs)) - for _, log := range logs { - jLog := &j.Log{ - Timestamp: log.Timestamp.UnixNano() / 1000, - Fields: ja.ConvertLogsToJaegerTags(log.Fields), - } - jLogs = append(jLogs, jLog) - } - return jLogs -} - -func buildReferences(references []Reference) []*j.SpanRef { - retMe := make([]*j.SpanRef, 0, len(references)) - for _, ref := range references { - if ref.Type == opentracing.ChildOfRef { - retMe = append(retMe, spanRef(ref.Context, j.SpanRefType_CHILD_OF)) - } else if ref.Type == opentracing.FollowsFromRef { - retMe = append(retMe, spanRef(ref.Context, j.SpanRefType_FOLLOWS_FROM)) - } - } - return retMe -} - -func spanRef(ctx SpanContext, refType j.SpanRefType) *j.SpanRef { - return &j.SpanRef{ - RefType: refType, - TraceIdLow: int64(ctx.traceID.Low), - TraceIdHigh: int64(ctx.traceID.High), - SpanId: int64(ctx.spanID), - } -} diff --git a/pkg/net/trace/jaeger/jaeger.go b/pkg/net/trace/jaeger/jaeger.go deleted file mode 100644 index 5feda289e..000000000 --- a/pkg/net/trace/jaeger/jaeger.go +++ /dev/null @@ -1,49 +0,0 @@ -package jaeger - -import ( - "github.com/go-kratos/kratos/pkg/log" - "github.com/go-kratos/kratos/pkg/net/trace" -) - -type Config struct { - Endpoint string - BatchSize int -} - -type JaegerReporter struct { - transport *HTTPTransport -} - -func newReport(c *Config) *JaegerReporter { - transport := NewHTTPTransport(c.Endpoint) - transport.batchSize = c.BatchSize - return &JaegerReporter{transport: transport} -} - -func (r *JaegerReporter) WriteSpan(raw *trace.Span) (err error) { - ctx := raw.Context() - traceID := TraceID{Low: ctx.TraceID} - spanID := SpanID(ctx.SpanID) - parentID := SpanID(ctx.ParentID) - tags := raw.Tags() - log.Info("[info] write span") - span := &Span{ - context: NewSpanContext(traceID, spanID, parentID, true, nil), - operationName: raw.OperationName(), - startTime: raw.StartTime(), - duration: raw.Duration(), - } - - span.serviceName = raw.ServiceName() - - for _, t := range tags { - span.SetTag(t.Key, t.Value) - } - - r.transport.Append(span) - return nil -} - -func (rpt *JaegerReporter) Close() error { - return rpt.transport.Close() -} diff --git a/pkg/net/trace/jaeger/jaeger_test.go b/pkg/net/trace/jaeger/jaeger_test.go deleted file mode 100644 index d082eb8ea..000000000 --- a/pkg/net/trace/jaeger/jaeger_test.go +++ /dev/null @@ -1,53 +0,0 @@ -package jaeger - -import ( - "io/ioutil" - "net/http" - "net/http/httptest" - "testing" - - "github.com/go-kratos/kratos/pkg/net/trace" -) - -func TestJaegerReporter(t *testing.T) { - var handler = func(w http.ResponseWriter, r *http.Request) { - if r.Method != "POST" { - t.Errorf("expected 'POST' request, got '%s'", r.Method) - } - - aSpanPayload, err := ioutil.ReadAll(r.Body) - if err != nil { - t.Errorf("unexpected error: %s", err.Error()) - } - - t.Logf("%s\n", aSpanPayload) - } - ht := httptest.NewServer(http.HandlerFunc(handler)) - defer ht.Close() - - c := &Config{ - Endpoint: ht.URL, - BatchSize: 1, - } - - //c.Endpoint = "http://127.0.0.1:14268/api/traces" - - report := newReport(c) - t1 := trace.NewTracer("jaeger_test_1", report, true) - t2 := trace.NewTracer("jaeger_test_2", report, true) - sp1 := t1.New("option_1") - sp2 := sp1.Fork("service3", "opt_client") - sp2.SetLog(trace.Log("log_k", "log_v")) - // inject - header := make(http.Header) - t1.Inject(sp2, trace.HTTPFormat, header) - t.Log(header) - sp3, err := t2.Extract(trace.HTTPFormat, header) - if err != nil { - t.Fatal(err) - } - sp3.Finish(nil) - sp2.Finish(nil) - sp1.Finish(nil) - report.Close() -} diff --git a/pkg/net/trace/jaeger/reference.go b/pkg/net/trace/jaeger/reference.go deleted file mode 100644 index becc01685..000000000 --- a/pkg/net/trace/jaeger/reference.go +++ /dev/null @@ -1,9 +0,0 @@ -package jaeger - -import "github.com/opentracing/opentracing-go" - -// Reference represents a causal reference to other Spans (via their SpanContext). -type Reference struct { - Type opentracing.SpanReferenceType - Context SpanContext -} diff --git a/pkg/net/trace/jaeger/span.go b/pkg/net/trace/jaeger/span.go deleted file mode 100644 index 33dab4ebe..000000000 --- a/pkg/net/trace/jaeger/span.go +++ /dev/null @@ -1,345 +0,0 @@ -package jaeger - -import ( - "sync" - "sync/atomic" - "time" - - "github.com/opentracing/opentracing-go" - "github.com/opentracing/opentracing-go/log" -) - -// Span implements opentracing.Span -type Span struct { - // referenceCounter used to increase the lifetime of - // the object before return it into the pool. - referenceCounter int32 - - serviceName string - - sync.RWMutex - - // TODO: (breaking change) change to use a pointer - context SpanContext - - // The name of the "operation" this span is an instance of. - // Known as a "span name" in some implementations. - operationName string - - // firstInProcess, if true, indicates that this span is the root of the (sub)tree - // of spans in the current process. In other words it's true for the root spans, - // and the ingress spans when the process joins another trace. - firstInProcess bool - - // startTime is the timestamp indicating when the span began, with microseconds precision. - startTime time.Time - - // duration returns duration of the span with microseconds precision. - // Zero value means duration is unknown. - duration time.Duration - - // tags attached to this span - tags []Tag - - // The span's "micro-log" - logs []opentracing.LogRecord - - // The number of logs dropped because of MaxLogsPerSpan. - numDroppedLogs int - - // references for this span - references []Reference -} - -// Tag is a simple key value wrapper. -// TODO (breaking change) deprecate in the next major release, use opentracing.Tag instead. -type Tag struct { - key string - value interface{} -} - -// NewTag creates a new Tag. -// TODO (breaking change) deprecate in the next major release, use opentracing.Tag instead. -func NewTag(key string, value interface{}) Tag { - return Tag{key: key, value: value} -} - -// SetOperationName sets or changes the operation name. -func (s *Span) SetOperationName(operationName string) opentracing.Span { - s.Lock() - s.operationName = operationName - s.Unlock() - return s -} - -// SetTag implements SetTag() of opentracing.Span -func (s *Span) SetTag(key string, value interface{}) opentracing.Span { - return s.setTagInternal(key, value, true) -} - -func (s *Span) setTagInternal(key string, value interface{}, lock bool) opentracing.Span { - if lock { - s.Lock() - defer s.Unlock() - } - s.appendTagNoLocking(key, value) - return s -} - -// SpanContext returns span context -func (s *Span) SpanContext() SpanContext { - s.Lock() - defer s.Unlock() - return s.context -} - -// StartTime returns span start time -func (s *Span) StartTime() time.Time { - s.Lock() - defer s.Unlock() - return s.startTime -} - -// Duration returns span duration -func (s *Span) Duration() time.Duration { - s.Lock() - defer s.Unlock() - return s.duration -} - -// Tags returns tags for span -func (s *Span) Tags() opentracing.Tags { - s.Lock() - defer s.Unlock() - var result = make(opentracing.Tags, len(s.tags)) - for _, tag := range s.tags { - result[tag.key] = tag.value - } - return result -} - -// Logs returns micro logs for span -func (s *Span) Logs() []opentracing.LogRecord { - s.Lock() - defer s.Unlock() - - logs := append([]opentracing.LogRecord(nil), s.logs...) - if s.numDroppedLogs != 0 { - fixLogs(logs, s.numDroppedLogs) - } - - return logs -} - -// References returns references for this span -func (s *Span) References() []opentracing.SpanReference { - s.Lock() - defer s.Unlock() - - if s.references == nil || len(s.references) == 0 { - return nil - } - - result := make([]opentracing.SpanReference, len(s.references)) - for i, r := range s.references { - result[i] = opentracing.SpanReference{Type: r.Type, ReferencedContext: r.Context} - } - return result -} - -func (s *Span) appendTagNoLocking(key string, value interface{}) { - s.tags = append(s.tags, Tag{key: key, value: value}) -} - -// LogFields implements opentracing.Span API -func (s *Span) LogFields(fields ...log.Field) { - s.Lock() - defer s.Unlock() - if !s.context.IsSampled() { - return - } - s.logFieldsNoLocking(fields...) -} - -// this function should only be called while holding a Write lock -func (s *Span) logFieldsNoLocking(fields ...log.Field) { - lr := opentracing.LogRecord{ - Fields: fields, - Timestamp: time.Now(), - } - s.appendLogNoLocking(lr) -} - -// LogKV implements opentracing.Span API -func (s *Span) LogKV(alternatingKeyValues ...interface{}) { - s.RLock() - sampled := s.context.IsSampled() - s.RUnlock() - if !sampled { - return - } - fields, err := log.InterleavedKVToFields(alternatingKeyValues...) - if err != nil { - s.LogFields(log.Error(err), log.String("function", "LogKV")) - return - } - s.LogFields(fields...) -} - -// LogEvent implements opentracing.Span API -func (s *Span) LogEvent(event string) { - s.Log(opentracing.LogData{Event: event}) -} - -// LogEventWithPayload implements opentracing.Span API -func (s *Span) LogEventWithPayload(event string, payload interface{}) { - s.Log(opentracing.LogData{Event: event, Payload: payload}) -} - -// Log implements opentracing.Span API -func (s *Span) Log(ld opentracing.LogData) { - s.Lock() - defer s.Unlock() - if s.context.IsSampled() { - s.appendLogNoLocking(ld.ToLogRecord()) - } -} - -// this function should only be called while holding a Write lock -func (s *Span) appendLogNoLocking(lr opentracing.LogRecord) { - maxLogs := 100 - // We have too many logs. We don't touch the first numOld logs; we treat the - // rest as a circular buffer and overwrite the oldest log among those. - numOld := (maxLogs - 1) / 2 - numNew := maxLogs - numOld - s.logs[numOld+s.numDroppedLogs%numNew] = lr - s.numDroppedLogs++ -} - -// rotateLogBuffer rotates the records in the buffer: records 0 to pos-1 move at -// the end (i.e. pos circular left shifts). -func rotateLogBuffer(buf []opentracing.LogRecord, pos int) { - // This algorithm is described in: - // http://www.cplusplus.com/reference/algorithm/rotate - for first, middle, next := 0, pos, pos; first != middle; { - buf[first], buf[next] = buf[next], buf[first] - first++ - next++ - if next == len(buf) { - next = middle - } else if first == middle { - middle = next - } - } -} - -func fixLogs(logs []opentracing.LogRecord, numDroppedLogs int) { - // We dropped some log events, which means that we used part of Logs as a - // circular buffer (see appendLog). De-circularize it. - numOld := (len(logs) - 1) / 2 - numNew := len(logs) - numOld - rotateLogBuffer(logs[numOld:], numDroppedLogs%numNew) - - // Replace the log in the middle (the oldest "new" log) with information - // about the dropped logs. This means that we are effectively dropping one - // more "new" log. - numDropped := numDroppedLogs + 1 - logs[numOld] = opentracing.LogRecord{ - // Keep the timestamp of the last dropped event. - Timestamp: logs[numOld].Timestamp, - Fields: []log.Field{ - log.String("event", "dropped Span logs"), - log.Int("dropped_log_count", numDropped), - log.String("component", "jaeger-client"), - }, - } -} - -func (s *Span) fixLogsIfDropped() { - if s.numDroppedLogs == 0 { - return - } - fixLogs(s.logs, s.numDroppedLogs) - s.numDroppedLogs = 0 -} - -// SetBaggageItem implements SetBaggageItem() of opentracing.SpanContext -func (s *Span) SetBaggageItem(key, value string) opentracing.Span { - s.context.baggage[key] = value - return s -} - -// BaggageItem implements BaggageItem() of opentracing.SpanContext -func (s *Span) BaggageItem(key string) string { - s.RLock() - defer s.RUnlock() - return s.context.baggage[key] -} - -// Finish implements opentracing.Span API -// After finishing the Span object it returns back to the allocator unless the reporter retains it again, -// so after that, the Span object should no longer be used because it won't be valid anymore. -func (s *Span) Finish() { - s.FinishWithOptions(opentracing.FinishOptions{}) -} - -// FinishWithOptions implements opentracing.Span API -func (s *Span) FinishWithOptions(options opentracing.FinishOptions) { -} - -// Context implements opentracing.Span API -func (s *Span) Context() opentracing.SpanContext { - s.Lock() - defer s.Unlock() - return s.context -} - -// Tracer implements opentracing.Span API -func (s *Span) Tracer() opentracing.Tracer { - return nil -} - -func (s *Span) String() string { - s.RLock() - defer s.RUnlock() - return s.context.String() -} - -// OperationName allows retrieving current operation name. -func (s *Span) OperationName() string { - s.RLock() - defer s.RUnlock() - return s.operationName -} - -// Retain increases object counter to increase the lifetime of the object -func (s *Span) Retain() *Span { - atomic.AddInt32(&s.referenceCounter, 1) - return s -} - -// Release decrements object counter and return to the -// allocator manager when counter will below zero -func (s *Span) Release() { - -} - -// reset span state and release unused data -func (s *Span) reset() { - s.firstInProcess = false - s.context = emptyContext - s.operationName = "" - s.startTime = time.Time{} - s.duration = 0 - atomic.StoreInt32(&s.referenceCounter, 0) - - // Note: To reuse memory we can save the pointers on the heap - s.tags = s.tags[:0] - s.logs = s.logs[:0] - s.numDroppedLogs = 0 - s.references = s.references[:0] -} - -func (s *Span) ServiceName() string { - return s.serviceName -} diff --git a/pkg/net/trace/jaeger/span_context.go b/pkg/net/trace/jaeger/span_context.go deleted file mode 100644 index d580c2d7f..000000000 --- a/pkg/net/trace/jaeger/span_context.go +++ /dev/null @@ -1,369 +0,0 @@ -package jaeger - -import ( - "errors" - "fmt" - "strconv" - "strings" - "sync" - - "go.uber.org/atomic" -) - -const ( - flagSampled = 1 - flagDebug = 2 - flagFirehose = 8 -) - -var ( - errEmptyTracerStateString = errors.New("Cannot convert empty string to tracer state") - errMalformedTracerStateString = errors.New("String does not match tracer state format") - - emptyContext = SpanContext{} -) - -// TraceID represents unique 128bit identifier of a trace -type TraceID struct { - High, Low uint64 -} - -// SpanID represents unique 64bit identifier of a span -type SpanID uint64 - -// SpanContext represents propagated span identity and state -type SpanContext struct { - // traceID represents globally unique ID of the trace. - // Usually generated as a random number. - traceID TraceID - - // spanID represents span ID that must be unique within its trace, - // but does not have to be globally unique. - spanID SpanID - - // parentID refers to the ID of the parent span. - // Should be 0 if the current span is a root span. - parentID SpanID - - // Distributed Context baggage. The is a snapshot in time. - baggage map[string]string - - // debugID can be set to some correlation ID when the context is being - // extracted from a TextMap carrier. - // - // See JaegerDebugHeader in constants.go - debugID string - - // samplingState is shared across all spans - samplingState *samplingState - - // remote indicates that span context represents a remote parent - remote bool -} - -type samplingState struct { - // Span context's state flags that are propagated across processes. Only lower 8 bits are used. - // We use an int32 instead of byte to be able to use CAS operations. - stateFlags atomic.Int32 - - // When state is not final, sampling will be retried on other span write operations, - // like SetOperationName / SetTag, and the spans will remain writable. - final atomic.Bool - - // localRootSpan stores the SpanID of the first span created in this process for a given trace. - localRootSpan SpanID - - // extendedState allows samplers to keep intermediate state. - // The keys and values in this map are completely opaque: interface{} -> interface{}. - extendedState sync.Map -} - -func (s *samplingState) isLocalRootSpan(id SpanID) bool { - return id == s.localRootSpan -} - -func (s *samplingState) setFlag(newFlag int32) { - swapped := false - for !swapped { - old := s.stateFlags.Load() - swapped = s.stateFlags.CAS(old, old|newFlag) - } -} - -func (s *samplingState) unsetFlag(newFlag int32) { - swapped := false - for !swapped { - old := s.stateFlags.Load() - swapped = s.stateFlags.CAS(old, old&^newFlag) - } -} - -func (s *samplingState) setSampled() { - s.setFlag(flagSampled) -} - -func (s *samplingState) unsetSampled() { - s.unsetFlag(flagSampled) -} - -func (s *samplingState) setDebugAndSampled() { - s.setFlag(flagDebug | flagSampled) -} - -func (s *samplingState) setFirehose() { - s.setFlag(flagFirehose) -} - -func (s *samplingState) setFlags(flags byte) { - s.stateFlags.Store(int32(flags)) -} - -func (s *samplingState) setFinal() { - s.final.Store(true) -} - -func (s *samplingState) flags() byte { - return byte(s.stateFlags.Load()) -} - -func (s *samplingState) isSampled() bool { - return s.stateFlags.Load()&flagSampled == flagSampled -} - -func (s *samplingState) isDebug() bool { - return s.stateFlags.Load()&flagDebug == flagDebug -} - -func (s *samplingState) isFirehose() bool { - return s.stateFlags.Load()&flagFirehose == flagFirehose -} - -func (s *samplingState) isFinal() bool { - return s.final.Load() -} - -func (s *samplingState) extendedStateForKey(key interface{}, initValue func() interface{}) interface{} { - if value, ok := s.extendedState.Load(key); ok { - return value - } - value := initValue() - value, _ = s.extendedState.LoadOrStore(key, value) - return value -} - -// ForeachBaggageItem implements ForeachBaggageItem() of opentracing.SpanContext -func (c SpanContext) ForeachBaggageItem(handler func(k, v string) bool) { - for k, v := range c.baggage { - if !handler(k, v) { - break - } - } -} - -// IsSampled returns whether this trace was chosen for permanent storage -// by the sampling mechanism of the tracer. -func (c SpanContext) IsSampled() bool { - return c.samplingState.isSampled() -} - -// IsDebug indicates whether sampling was explicitly requested by the service. -func (c SpanContext) IsDebug() bool { - return c.samplingState.isDebug() -} - -// IsSamplingFinalized indicates whether the sampling decision has been finalized. -func (c SpanContext) IsSamplingFinalized() bool { - return c.samplingState.isFinal() -} - -// IsFirehose indicates whether the firehose flag was set -func (c SpanContext) IsFirehose() bool { - return c.samplingState.isFirehose() -} - -// ExtendedSamplingState returns the custom state object for a given key. If the value for this key does not exist, -// it is initialized via initValue function. This state can be used by samplers (e.g. x.PrioritySampler). -func (c SpanContext) ExtendedSamplingState(key interface{}, initValue func() interface{}) interface{} { - return c.samplingState.extendedStateForKey(key, initValue) -} - -// IsValid indicates whether this context actually represents a valid trace. -func (c SpanContext) IsValid() bool { - return c.traceID.IsValid() && c.spanID != 0 -} - -// SetFirehose enables firehose mode for this trace. -func (c SpanContext) SetFirehose() { - c.samplingState.setFirehose() -} - -func (c SpanContext) String() string { - if c.traceID.High == 0 { - return fmt.Sprintf("%016x:%016x:%016x:%x", c.traceID.Low, uint64(c.spanID), uint64(c.parentID), c.samplingState.stateFlags.Load()) - } - return fmt.Sprintf("%016x%016x:%016x:%016x:%x", c.traceID.High, c.traceID.Low, uint64(c.spanID), uint64(c.parentID), c.samplingState.stateFlags.Load()) -} - -// ContextFromString reconstructs the Context encoded in a string -func ContextFromString(value string) (SpanContext, error) { - var context SpanContext - if value == "" { - return emptyContext, errEmptyTracerStateString - } - parts := strings.Split(value, ":") - if len(parts) != 4 { - return emptyContext, errMalformedTracerStateString - } - var err error - if context.traceID, err = TraceIDFromString(parts[0]); err != nil { - return emptyContext, err - } - if context.spanID, err = SpanIDFromString(parts[1]); err != nil { - return emptyContext, err - } - if context.parentID, err = SpanIDFromString(parts[2]); err != nil { - return emptyContext, err - } - flags, err := strconv.ParseUint(parts[3], 10, 8) - if err != nil { - return emptyContext, err - } - context.samplingState = &samplingState{} - context.samplingState.setFlags(byte(flags)) - return context, nil -} - -// TraceID returns the trace ID of this span context -func (c SpanContext) TraceID() TraceID { - return c.traceID -} - -// SpanID returns the span ID of this span context -func (c SpanContext) SpanID() SpanID { - return c.spanID -} - -// ParentID returns the parent span ID of this span context -func (c SpanContext) ParentID() SpanID { - return c.parentID -} - -// Flags returns the bitmap containing such bits as 'sampled' and 'debug'. -func (c SpanContext) Flags() byte { - return c.samplingState.flags() -} - -// NewSpanContext creates a new instance of SpanContext -func NewSpanContext(traceID TraceID, spanID, parentID SpanID, sampled bool, baggage map[string]string) SpanContext { - samplingState := &samplingState{} - if sampled { - samplingState.setSampled() - } - - return SpanContext{ - traceID: traceID, - spanID: spanID, - parentID: parentID, - samplingState: samplingState, - baggage: baggage} -} - -// CopyFrom copies data from ctx into this context, including span identity and baggage. -// TODO This is only used by interop.go. Remove once TChannel Go supports OpenTracing. -func (c *SpanContext) CopyFrom(ctx *SpanContext) { - c.traceID = ctx.traceID - c.spanID = ctx.spanID - c.parentID = ctx.parentID - c.samplingState = ctx.samplingState - if l := len(ctx.baggage); l > 0 { - c.baggage = make(map[string]string, l) - for k, v := range ctx.baggage { - c.baggage[k] = v - } - } else { - c.baggage = nil - } -} - -// WithBaggageItem creates a new context with an extra baggage item. -func (c SpanContext) WithBaggageItem(key, value string) SpanContext { - var newBaggage map[string]string - if c.baggage == nil { - newBaggage = map[string]string{key: value} - } else { - newBaggage = make(map[string]string, len(c.baggage)+1) - for k, v := range c.baggage { - newBaggage[k] = v - } - newBaggage[key] = value - } - // Use positional parameters so the compiler will help catch new fields. - return SpanContext{c.traceID, c.spanID, c.parentID, newBaggage, "", c.samplingState, c.remote} -} - -// isDebugIDContainerOnly returns true when the instance of the context is only -// used to return the debug/correlation ID from extract() method. This happens -// in the situation when "jaeger-debug-id" header is passed in the carrier to -// the extract() method, but the request otherwise has no span context in it. -// Previously this would've returned opentracing.ErrSpanContextNotFound from the -// extract method, but now it returns a dummy context with only debugID filled in. -// -// See JaegerDebugHeader in constants.go -// See TextMapPropagator#Extract -func (c *SpanContext) isDebugIDContainerOnly() bool { - return !c.traceID.IsValid() && c.debugID != "" -} - -// ------- TraceID ------- - -func (t TraceID) String() string { - if t.High == 0 { - return fmt.Sprintf("%x", t.Low) - } - return fmt.Sprintf("%x%016x", t.High, t.Low) -} - -// TraceIDFromString creates a TraceID from a hexadecimal string -func TraceIDFromString(s string) (TraceID, error) { - var hi, lo uint64 - var err error - if len(s) > 32 { - return TraceID{}, fmt.Errorf("TraceID cannot be longer than 32 hex characters: %s", s) - } else if len(s) > 16 { - hiLen := len(s) - 16 - if hi, err = strconv.ParseUint(s[0:hiLen], 16, 64); err != nil { - return TraceID{}, err - } - if lo, err = strconv.ParseUint(s[hiLen:], 16, 64); err != nil { - return TraceID{}, err - } - } else { - if lo, err = strconv.ParseUint(s, 16, 64); err != nil { - return TraceID{}, err - } - } - return TraceID{High: hi, Low: lo}, nil -} - -// IsValid checks if the trace ID is valid, i.e. not zero. -func (t TraceID) IsValid() bool { - return t.High != 0 || t.Low != 0 -} - -// ------- SpanID ------- - -func (s SpanID) String() string { - return fmt.Sprintf("%x", uint64(s)) -} - -// SpanIDFromString creates a SpanID from a hexadecimal string -func SpanIDFromString(s string) (SpanID, error) { - if len(s) > 16 { - return SpanID(0), fmt.Errorf("SpanID cannot be longer than 16 hex characters: %s", s) - } - id, err := strconv.ParseUint(s, 16, 64) - if err != nil { - return SpanID(0), err - } - return SpanID(id), nil -} diff --git a/pkg/net/trace/marshal.go b/pkg/net/trace/marshal.go deleted file mode 100644 index d92c484da..000000000 --- a/pkg/net/trace/marshal.go +++ /dev/null @@ -1,106 +0,0 @@ -package trace - -import ( - "encoding/binary" - errs "errors" - "fmt" - "math" - "time" - - "github.com/golang/protobuf/proto" - "github.com/golang/protobuf/ptypes/duration" - "github.com/golang/protobuf/ptypes/timestamp" - - protogen "github.com/go-kratos/kratos/pkg/net/trace/proto" -) - -const protoVersion1 int32 = 1 - -var ( - errSpanVersion = errs.New("trace: marshal not support version") -) - -func marshalSpan(sp *Span, version int32) ([]byte, error) { - if version == protoVersion1 { - return marshalSpanV1(sp) - } - return nil, errSpanVersion -} - -func marshalSpanV1(sp *Span) ([]byte, error) { - protoSpan := new(protogen.Span) - protoSpan.Version = protoVersion1 - protoSpan.ServiceName = sp.dapper.serviceName - protoSpan.OperationName = sp.operationName - protoSpan.TraceId = sp.context.TraceID - protoSpan.SpanId = sp.context.SpanID - protoSpan.ParentId = sp.context.ParentID - protoSpan.SamplingProbability = sp.context.Probability - protoSpan.StartTime = ×tamp.Timestamp{ - Seconds: sp.startTime.Unix(), - Nanos: int32(sp.startTime.Nanosecond()), - } - protoSpan.Duration = &duration.Duration{ - Seconds: int64(sp.duration / time.Second), - Nanos: int32(sp.duration % time.Second), - } - protoSpan.Tags = make([]*protogen.Tag, len(sp.tags)) - for i := range sp.tags { - protoSpan.Tags[i] = toProtoTag(sp.tags[i]) - } - protoSpan.Logs = sp.logs - return proto.Marshal(protoSpan) -} - -func toProtoTag(tag Tag) *protogen.Tag { - ptag := &protogen.Tag{Key: tag.Key} - switch value := tag.Value.(type) { - case string: - ptag.Kind = protogen.Tag_STRING - ptag.Value = []byte(value) - case int: - ptag.Kind = protogen.Tag_INT - ptag.Value = serializeInt64(int64(value)) - case int32: - ptag.Kind = protogen.Tag_INT - ptag.Value = serializeInt64(int64(value)) - case int64: - ptag.Kind = protogen.Tag_INT - ptag.Value = serializeInt64(value) - case bool: - ptag.Kind = protogen.Tag_BOOL - ptag.Value = serializeBool(value) - case float32: - ptag.Kind = protogen.Tag_BOOL - ptag.Value = serializeFloat64(float64(value)) - case float64: - ptag.Kind = protogen.Tag_BOOL - ptag.Value = serializeFloat64(value) - default: - ptag.Kind = protogen.Tag_STRING - ptag.Value = []byte((fmt.Sprintf("%v", tag.Value))) - } - return ptag -} - -func serializeInt64(v int64) []byte { - data := make([]byte, 8) - binary.BigEndian.PutUint64(data, uint64(v)) - return data -} - -func serializeFloat64(v float64) []byte { - data := make([]byte, 8) - binary.BigEndian.PutUint64(data, math.Float64bits(v)) - return data -} - -func serializeBool(v bool) []byte { - data := make([]byte, 1) - if v { - data[0] = byte(1) - } else { - data[0] = byte(0) - } - return data -} diff --git a/pkg/net/trace/marshal_test.go b/pkg/net/trace/marshal_test.go deleted file mode 100644 index 3c4d5831d..000000000 --- a/pkg/net/trace/marshal_test.go +++ /dev/null @@ -1,18 +0,0 @@ -package trace - -import ( - "testing" -) - -func TestMarshalSpanV1(t *testing.T) { - report := &mockReport{} - t1 := NewTracer("service1", report, true) - sp1 := t1.New("opt_test").(*Span) - sp1.SetLog(Log("hello", "test123")) - sp1.SetTag(TagString("tag1", "hell"), TagBool("booltag", true), TagFloat64("float64tag", 3.14159)) - sp1.Finish(nil) - _, err := marshalSpanV1(sp1) - if err != nil { - t.Error(err) - } -} diff --git a/pkg/net/trace/mocktrace/mocktrace.go b/pkg/net/trace/mocktrace/mocktrace.go deleted file mode 100644 index 4fcf2c561..000000000 --- a/pkg/net/trace/mocktrace/mocktrace.go +++ /dev/null @@ -1,84 +0,0 @@ -package mocktrace - -import ( - "github.com/go-kratos/kratos/pkg/net/trace" -) - -// MockTrace . -type MockTrace struct { - Spans []*MockSpan -} - -// New . -func (m *MockTrace) New(operationName string, opts ...trace.Option) trace.Trace { - span := &MockSpan{OperationName: operationName, MockTrace: m} - m.Spans = append(m.Spans, span) - return span -} - -// Inject . -func (m *MockTrace) Inject(t trace.Trace, format interface{}, carrier interface{}) error { - return nil -} - -// Extract . -func (m *MockTrace) Extract(format interface{}, carrier interface{}) (trace.Trace, error) { - return &MockSpan{}, nil -} - -// MockSpan . -type MockSpan struct { - *MockTrace - OperationName string - FinishErr error - Finished bool - Tags []trace.Tag - Logs []trace.LogField -} - -// Fork . -func (m *MockSpan) Fork(serviceName string, operationName string) trace.Trace { - span := &MockSpan{OperationName: operationName, MockTrace: m.MockTrace} - m.Spans = append(m.Spans, span) - return span -} - -// Follow . -func (m *MockSpan) Follow(serviceName string, operationName string) trace.Trace { - span := &MockSpan{OperationName: operationName, MockTrace: m.MockTrace} - m.Spans = append(m.Spans, span) - return span -} - -// Finish . -func (m *MockSpan) Finish(perr *error) { - if perr != nil { - m.FinishErr = *perr - } - m.Finished = true -} - -// SetTag . -func (m *MockSpan) SetTag(tags ...trace.Tag) trace.Trace { - m.Tags = append(m.Tags, tags...) - return m -} - -// SetLog . -func (m *MockSpan) SetLog(logs ...trace.LogField) trace.Trace { - m.Logs = append(m.Logs, logs...) - return m -} - -// Visit . -func (m *MockSpan) Visit(fn func(k, v string)) {} - -// SetTitle . -func (m *MockSpan) SetTitle(title string) { - m.OperationName = title -} - -// TraceID . -func (m *MockSpan) TraceID() string { - return "" -} diff --git a/pkg/net/trace/mocktrace/mocktrace_test.go b/pkg/net/trace/mocktrace/mocktrace_test.go deleted file mode 100644 index a0021f1ef..000000000 --- a/pkg/net/trace/mocktrace/mocktrace_test.go +++ /dev/null @@ -1,24 +0,0 @@ -// Package mocktrace this ut just make ci happay. -package mocktrace - -import ( - "fmt" - "testing" -) - -func TestMockTrace(t *testing.T) { - mocktrace := &MockTrace{} - mocktrace.Inject(nil, nil, nil) - mocktrace.Extract(nil, nil) - - root := mocktrace.New("test") - root.Fork("", "") - root.Follow("", "") - root.Finish(nil) - err := fmt.Errorf("test") - root.Finish(&err) - root.SetTag() - root.SetLog() - root.Visit(func(k, v string) {}) - root.SetTitle("") -} diff --git a/pkg/net/trace/noop.go b/pkg/net/trace/noop.go deleted file mode 100644 index b60a32ba9..000000000 --- a/pkg/net/trace/noop.go +++ /dev/null @@ -1,47 +0,0 @@ -package trace - -var ( - _ Tracer = nooptracer{} -) - -type nooptracer struct{} - -func (n nooptracer) New(title string, opts ...Option) Trace { - return noopspan{} -} - -func (n nooptracer) Inject(t Trace, format interface{}, carrier interface{}) error { - return nil -} - -func (n nooptracer) Extract(format interface{}, carrier interface{}) (Trace, error) { - return noopspan{}, nil -} - -type noopspan struct{} - -func (n noopspan) TraceID() string { return "" } - -func (n noopspan) Fork(string, string) Trace { - return noopspan{} -} - -func (n noopspan) Follow(string, string) Trace { - return noopspan{} -} - -func (n noopspan) Finish(err *error) {} - -func (n noopspan) SetTag(tags ...Tag) Trace { - return noopspan{} -} - -func (n noopspan) SetLog(logs ...LogField) Trace { - return noopspan{} -} - -func (n noopspan) Visit(func(k, v string)) {} - -func (n noopspan) SetTitle(string) {} - -func (n noopspan) String() string { return "" } diff --git a/pkg/net/trace/option.go b/pkg/net/trace/option.go deleted file mode 100644 index f0865c2d7..000000000 --- a/pkg/net/trace/option.go +++ /dev/null @@ -1,17 +0,0 @@ -package trace - -var defaultOption = option{} - -type option struct { - Debug bool -} - -// Option dapper Option -type Option func(*option) - -// EnableDebug enable debug mode -func EnableDebug() Option { - return func(opt *option) { - opt.Debug = true - } -} diff --git a/pkg/net/trace/propagation.go b/pkg/net/trace/propagation.go deleted file mode 100644 index 0eb45bd23..000000000 --- a/pkg/net/trace/propagation.go +++ /dev/null @@ -1,177 +0,0 @@ -package trace - -import ( - errs "errors" - "net/http" - - "google.golang.org/grpc/metadata" -) - -var ( - // ErrUnsupportedFormat occurs when the `format` passed to Tracer.Inject() or - // Tracer.Extract() is not recognized by the Tracer implementation. - ErrUnsupportedFormat = errs.New("trace: Unknown or unsupported Inject/Extract format") - - // ErrTraceNotFound occurs when the `carrier` passed to - // Tracer.Extract() is valid and uncorrupted but has insufficient - // information to extract a Trace. - ErrTraceNotFound = errs.New("trace: Trace not found in Extract carrier") - - // ErrInvalidTrace errors occur when Tracer.Inject() is asked to - // operate on a Trace which it is not prepared to handle (for - // example, since it was created by a different tracer implementation). - ErrInvalidTrace = errs.New("trace: Trace type incompatible with tracer") - - // ErrInvalidCarrier errors occur when Tracer.Inject() or Tracer.Extract() - // implementations expect a different type of `carrier` than they are - // given. - ErrInvalidCarrier = errs.New("trace: Invalid Inject/Extract carrier") - - // ErrTraceCorrupted occurs when the `carrier` passed to - // Tracer.Extract() is of the expected type but is corrupted. - ErrTraceCorrupted = errs.New("trace: Trace data corrupted in Extract carrier") -) - -// BuiltinFormat is used to demarcate the values within package `trace` -// that are intended for use with the Tracer.Inject() and Tracer.Extract() -// methods. -type BuiltinFormat byte - -// support format list -const ( - // HTTPFormat represents Trace as HTTP header string pairs. - // - // the HTTPFormat format requires that the keys and values - // be valid as HTTP headers as-is (i.e., character casing may be unstable - // and special characters are disallowed in keys, values should be - // URL-escaped, etc). - // - // the carrier must be a `http.Header`. - HTTPFormat BuiltinFormat = iota - // GRPCFormat represents Trace as gRPC metadata. - // - // the carrier must be a `google.golang.org/grpc/metadata.MD`. - GRPCFormat -) - -// Carrier propagator must convert generic interface{} to something this -// implement Carrier interface, Trace can use Carrier to represents itself. -type Carrier interface { - Set(key, val string) - Get(key string) string -} - -// propagator is responsible for injecting and extracting `Trace` instances -// from a format-specific "carrier" -type propagator interface { - Inject(carrier interface{}) (Carrier, error) - Extract(carrier interface{}) (Carrier, error) -} - -type httpPropagator struct{} - -type httpCarrier http.Header - -func (h httpCarrier) Set(key, val string) { - http.Header(h).Set(key, val) -} - -func (h httpCarrier) Get(key string) string { - return http.Header(h).Get(key) -} - -func (httpPropagator) Inject(carrier interface{}) (Carrier, error) { - header, ok := carrier.(http.Header) - if !ok { - return nil, ErrInvalidCarrier - } - if header == nil { - return nil, ErrInvalidTrace - } - return httpCarrier(header), nil -} - -func (httpPropagator) Extract(carrier interface{}) (Carrier, error) { - header, ok := carrier.(http.Header) - if !ok { - return nil, ErrInvalidCarrier - } - if header == nil { - return nil, ErrTraceNotFound - } - return httpCarrier(header), nil -} - -const legacyGRPCKey = "trace" - -type grpcPropagator struct{} - -type grpcCarrier map[string][]string - -func (g grpcCarrier) Get(key string) string { - if v, ok := g[key]; ok && len(v) > 0 { - return v[0] - } - // ts := g[legacyGRPCKey] - // if len(ts) != 8 { - // return "" - // } - // switch key { - // case KeyTraceID: - // return ts[0] - // case KeyTraceSpanID: - // return ts[1] - // case KeyTraceParentID: - // return ts[2] - // case KeyTraceLevel: - // return ts[3] - // case KeyTraceSampled: - // return ts[4] - // case KeyTraceCaller: - // return ts[5] - // } - return "" -} - -func (g grpcCarrier) Set(key, val string) { - // ts := make([]string, 8) - // g[legacyGRPCKey] = ts - // switch key { - // case KeyTraceID: - // ts[0] = val - // case KeyTraceSpanID: - // ts[1] = val - // case KeyTraceParentID: - // ts[2] = val - // case KeyTraceLevel: - // ts[3] = val - // case KeyTraceSampled: - // ts[4] = val - // case KeyTraceCaller: - // ts[5] = val - // default: - g[key] = append(g[key], val) - // } -} - -func (grpcPropagator) Inject(carrier interface{}) (Carrier, error) { - md, ok := carrier.(metadata.MD) - if !ok { - return nil, ErrInvalidCarrier - } - if md == nil { - return nil, ErrInvalidTrace - } - return grpcCarrier(md), nil -} - -func (grpcPropagator) Extract(carrier interface{}) (Carrier, error) { - md, ok := carrier.(metadata.MD) - if !ok { - return nil, ErrInvalidCarrier - } - if md == nil { - return nil, ErrTraceNotFound - } - return grpcCarrier(md), nil -} diff --git a/pkg/net/trace/proto/span.pb.go b/pkg/net/trace/proto/span.pb.go deleted file mode 100644 index 55f612fc9..000000000 --- a/pkg/net/trace/proto/span.pb.go +++ /dev/null @@ -1,557 +0,0 @@ -// Code generated by protoc-gen-go. DO NOT EDIT. -// source: proto/span.proto - -package protogen - -import proto "github.com/golang/protobuf/proto" -import fmt "fmt" -import math "math" -import duration "github.com/golang/protobuf/ptypes/duration" -import timestamp "github.com/golang/protobuf/ptypes/timestamp" - -// Reference imports to suppress errors if they are not otherwise used. -var _ = proto.Marshal -var _ = fmt.Errorf -var _ = math.Inf - -// This is a compile-time assertion to ensure that this generated file -// is compatible with the proto package it is being compiled against. -// A compilation error at this line likely means your copy of the -// proto package needs to be updated. -const _ = proto.ProtoPackageIsVersion2 // please upgrade the proto package - -type Tag_Kind int32 - -const ( - Tag_STRING Tag_Kind = 0 - Tag_INT Tag_Kind = 1 - Tag_BOOL Tag_Kind = 2 - Tag_FLOAT Tag_Kind = 3 -) - -var Tag_Kind_name = map[int32]string{ - 0: "STRING", - 1: "INT", - 2: "BOOL", - 3: "FLOAT", -} -var Tag_Kind_value = map[string]int32{ - "STRING": 0, - "INT": 1, - "BOOL": 2, - "FLOAT": 3, -} - -func (x Tag_Kind) String() string { - return proto.EnumName(Tag_Kind_name, int32(x)) -} -func (Tag_Kind) EnumDescriptor() ([]byte, []int) { - return fileDescriptor_span_68a8dae26ef502a2, []int{0, 0} -} - -type Log_Kind int32 - -const ( - Log_STRING Log_Kind = 0 - Log_INT Log_Kind = 1 - Log_BOOL Log_Kind = 2 - Log_FLOAT Log_Kind = 3 -) - -var Log_Kind_name = map[int32]string{ - 0: "STRING", - 1: "INT", - 2: "BOOL", - 3: "FLOAT", -} -var Log_Kind_value = map[string]int32{ - "STRING": 0, - "INT": 1, - "BOOL": 2, - "FLOAT": 3, -} - -func (x Log_Kind) String() string { - return proto.EnumName(Log_Kind_name, int32(x)) -} -func (Log_Kind) EnumDescriptor() ([]byte, []int) { - return fileDescriptor_span_68a8dae26ef502a2, []int{2, 0} -} - -type SpanRef_RefType int32 - -const ( - SpanRef_CHILD_OF SpanRef_RefType = 0 - SpanRef_FOLLOWS_FROM SpanRef_RefType = 1 -) - -var SpanRef_RefType_name = map[int32]string{ - 0: "CHILD_OF", - 1: "FOLLOWS_FROM", -} -var SpanRef_RefType_value = map[string]int32{ - "CHILD_OF": 0, - "FOLLOWS_FROM": 1, -} - -func (x SpanRef_RefType) String() string { - return proto.EnumName(SpanRef_RefType_name, int32(x)) -} -func (SpanRef_RefType) EnumDescriptor() ([]byte, []int) { - return fileDescriptor_span_68a8dae26ef502a2, []int{3, 0} -} - -type Tag struct { - Key string `protobuf:"bytes,1,opt,name=key" json:"key,omitempty"` - Kind Tag_Kind `protobuf:"varint,2,opt,name=kind,enum=dapper.trace.Tag_Kind" json:"kind,omitempty"` - Value []byte `protobuf:"bytes,3,opt,name=value,proto3" json:"value,omitempty"` - XXX_NoUnkeyedLiteral struct{} `json:"-"` - XXX_unrecognized []byte `json:"-"` - XXX_sizecache int32 `json:"-"` -} - -func (m *Tag) Reset() { *m = Tag{} } -func (m *Tag) String() string { return proto.CompactTextString(m) } -func (*Tag) ProtoMessage() {} -func (*Tag) Descriptor() ([]byte, []int) { - return fileDescriptor_span_68a8dae26ef502a2, []int{0} -} -func (m *Tag) XXX_Unmarshal(b []byte) error { - return xxx_messageInfo_Tag.Unmarshal(m, b) -} -func (m *Tag) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { - return xxx_messageInfo_Tag.Marshal(b, m, deterministic) -} -func (dst *Tag) XXX_Merge(src proto.Message) { - xxx_messageInfo_Tag.Merge(dst, src) -} -func (m *Tag) XXX_Size() int { - return xxx_messageInfo_Tag.Size(m) -} -func (m *Tag) XXX_DiscardUnknown() { - xxx_messageInfo_Tag.DiscardUnknown(m) -} - -var xxx_messageInfo_Tag proto.InternalMessageInfo - -func (m *Tag) GetKey() string { - if m != nil { - return m.Key - } - return "" -} - -func (m *Tag) GetKind() Tag_Kind { - if m != nil { - return m.Kind - } - return Tag_STRING -} - -func (m *Tag) GetValue() []byte { - if m != nil { - return m.Value - } - return nil -} - -type Field struct { - Key string `protobuf:"bytes,1,opt,name=key" json:"key,omitempty"` - Value []byte `protobuf:"bytes,2,opt,name=value,proto3" json:"value,omitempty"` - XXX_NoUnkeyedLiteral struct{} `json:"-"` - XXX_unrecognized []byte `json:"-"` - XXX_sizecache int32 `json:"-"` -} - -func (m *Field) Reset() { *m = Field{} } -func (m *Field) String() string { return proto.CompactTextString(m) } -func (*Field) ProtoMessage() {} -func (*Field) Descriptor() ([]byte, []int) { - return fileDescriptor_span_68a8dae26ef502a2, []int{1} -} -func (m *Field) XXX_Unmarshal(b []byte) error { - return xxx_messageInfo_Field.Unmarshal(m, b) -} -func (m *Field) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { - return xxx_messageInfo_Field.Marshal(b, m, deterministic) -} -func (dst *Field) XXX_Merge(src proto.Message) { - xxx_messageInfo_Field.Merge(dst, src) -} -func (m *Field) XXX_Size() int { - return xxx_messageInfo_Field.Size(m) -} -func (m *Field) XXX_DiscardUnknown() { - xxx_messageInfo_Field.DiscardUnknown(m) -} - -var xxx_messageInfo_Field proto.InternalMessageInfo - -func (m *Field) GetKey() string { - if m != nil { - return m.Key - } - return "" -} - -func (m *Field) GetValue() []byte { - if m != nil { - return m.Value - } - return nil -} - -type Log struct { - Key string `protobuf:"bytes,1,opt,name=key" json:"key,omitempty"` - Kind Log_Kind `protobuf:"varint,2,opt,name=kind,enum=dapper.trace.Log_Kind" json:"kind,omitempty"` - Value []byte `protobuf:"bytes,3,opt,name=value,proto3" json:"value,omitempty"` - Timestamp int64 `protobuf:"varint,4,opt,name=timestamp" json:"timestamp,omitempty"` - Fields []*Field `protobuf:"bytes,5,rep,name=fields" json:"fields,omitempty"` - XXX_NoUnkeyedLiteral struct{} `json:"-"` - XXX_unrecognized []byte `json:"-"` - XXX_sizecache int32 `json:"-"` -} - -func (m *Log) Reset() { *m = Log{} } -func (m *Log) String() string { return proto.CompactTextString(m) } -func (*Log) ProtoMessage() {} -func (*Log) Descriptor() ([]byte, []int) { - return fileDescriptor_span_68a8dae26ef502a2, []int{2} -} -func (m *Log) XXX_Unmarshal(b []byte) error { - return xxx_messageInfo_Log.Unmarshal(m, b) -} -func (m *Log) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { - return xxx_messageInfo_Log.Marshal(b, m, deterministic) -} -func (dst *Log) XXX_Merge(src proto.Message) { - xxx_messageInfo_Log.Merge(dst, src) -} -func (m *Log) XXX_Size() int { - return xxx_messageInfo_Log.Size(m) -} -func (m *Log) XXX_DiscardUnknown() { - xxx_messageInfo_Log.DiscardUnknown(m) -} - -var xxx_messageInfo_Log proto.InternalMessageInfo - -func (m *Log) GetKey() string { - if m != nil { - return m.Key - } - return "" -} - -func (m *Log) GetKind() Log_Kind { - if m != nil { - return m.Kind - } - return Log_STRING -} - -func (m *Log) GetValue() []byte { - if m != nil { - return m.Value - } - return nil -} - -func (m *Log) GetTimestamp() int64 { - if m != nil { - return m.Timestamp - } - return 0 -} - -func (m *Log) GetFields() []*Field { - if m != nil { - return m.Fields - } - return nil -} - -// SpanRef describes causal relationship of the current span to another span (e.g. 'child-of') -type SpanRef struct { - RefType SpanRef_RefType `protobuf:"varint,1,opt,name=ref_type,json=refType,enum=dapper.trace.SpanRef_RefType" json:"ref_type,omitempty"` - TraceId uint64 `protobuf:"varint,2,opt,name=trace_id,json=traceId" json:"trace_id,omitempty"` - SpanId uint64 `protobuf:"varint,3,opt,name=span_id,json=spanId" json:"span_id,omitempty"` - XXX_NoUnkeyedLiteral struct{} `json:"-"` - XXX_unrecognized []byte `json:"-"` - XXX_sizecache int32 `json:"-"` -} - -func (m *SpanRef) Reset() { *m = SpanRef{} } -func (m *SpanRef) String() string { return proto.CompactTextString(m) } -func (*SpanRef) ProtoMessage() {} -func (*SpanRef) Descriptor() ([]byte, []int) { - return fileDescriptor_span_68a8dae26ef502a2, []int{3} -} -func (m *SpanRef) XXX_Unmarshal(b []byte) error { - return xxx_messageInfo_SpanRef.Unmarshal(m, b) -} -func (m *SpanRef) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { - return xxx_messageInfo_SpanRef.Marshal(b, m, deterministic) -} -func (dst *SpanRef) XXX_Merge(src proto.Message) { - xxx_messageInfo_SpanRef.Merge(dst, src) -} -func (m *SpanRef) XXX_Size() int { - return xxx_messageInfo_SpanRef.Size(m) -} -func (m *SpanRef) XXX_DiscardUnknown() { - xxx_messageInfo_SpanRef.DiscardUnknown(m) -} - -var xxx_messageInfo_SpanRef proto.InternalMessageInfo - -func (m *SpanRef) GetRefType() SpanRef_RefType { - if m != nil { - return m.RefType - } - return SpanRef_CHILD_OF -} - -func (m *SpanRef) GetTraceId() uint64 { - if m != nil { - return m.TraceId - } - return 0 -} - -func (m *SpanRef) GetSpanId() uint64 { - if m != nil { - return m.SpanId - } - return 0 -} - -// Span represents a named unit of work performed by a service. -type Span struct { - Version int32 `protobuf:"varint,99,opt,name=version" json:"version,omitempty"` - ServiceName string `protobuf:"bytes,1,opt,name=service_name,json=serviceName" json:"service_name,omitempty"` - OperationName string `protobuf:"bytes,2,opt,name=operation_name,json=operationName" json:"operation_name,omitempty"` - // Deprecated: caller no long required - Caller string `protobuf:"bytes,3,opt,name=caller" json:"caller,omitempty"` - TraceId uint64 `protobuf:"varint,4,opt,name=trace_id,json=traceId" json:"trace_id,omitempty"` - SpanId uint64 `protobuf:"varint,5,opt,name=span_id,json=spanId" json:"span_id,omitempty"` - ParentId uint64 `protobuf:"varint,6,opt,name=parent_id,json=parentId" json:"parent_id,omitempty"` - // Deprecated: level no long required - Level int32 `protobuf:"varint,7,opt,name=level" json:"level,omitempty"` - // Deprecated: use start_time instead instead of start_at - StartAt int64 `protobuf:"varint,8,opt,name=start_at,json=startAt" json:"start_at,omitempty"` - // Deprecated: use duration instead instead of finish_at - FinishAt int64 `protobuf:"varint,9,opt,name=finish_at,json=finishAt" json:"finish_at,omitempty"` - SamplingProbability float32 `protobuf:"fixed32,10,opt,name=sampling_probability,json=samplingProbability" json:"sampling_probability,omitempty"` - Env string `protobuf:"bytes,19,opt,name=env" json:"env,omitempty"` - StartTime *timestamp.Timestamp `protobuf:"bytes,20,opt,name=start_time,json=startTime" json:"start_time,omitempty"` - Duration *duration.Duration `protobuf:"bytes,21,opt,name=duration" json:"duration,omitempty"` - References []*SpanRef `protobuf:"bytes,22,rep,name=references" json:"references,omitempty"` - Tags []*Tag `protobuf:"bytes,11,rep,name=tags" json:"tags,omitempty"` - Logs []*Log `protobuf:"bytes,12,rep,name=logs" json:"logs,omitempty"` - XXX_NoUnkeyedLiteral struct{} `json:"-"` - XXX_unrecognized []byte `json:"-"` - XXX_sizecache int32 `json:"-"` -} - -func (m *Span) Reset() { *m = Span{} } -func (m *Span) String() string { return proto.CompactTextString(m) } -func (*Span) ProtoMessage() {} -func (*Span) Descriptor() ([]byte, []int) { - return fileDescriptor_span_68a8dae26ef502a2, []int{4} -} -func (m *Span) XXX_Unmarshal(b []byte) error { - return xxx_messageInfo_Span.Unmarshal(m, b) -} -func (m *Span) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { - return xxx_messageInfo_Span.Marshal(b, m, deterministic) -} -func (dst *Span) XXX_Merge(src proto.Message) { - xxx_messageInfo_Span.Merge(dst, src) -} -func (m *Span) XXX_Size() int { - return xxx_messageInfo_Span.Size(m) -} -func (m *Span) XXX_DiscardUnknown() { - xxx_messageInfo_Span.DiscardUnknown(m) -} - -var xxx_messageInfo_Span proto.InternalMessageInfo - -func (m *Span) GetVersion() int32 { - if m != nil { - return m.Version - } - return 0 -} - -func (m *Span) GetServiceName() string { - if m != nil { - return m.ServiceName - } - return "" -} - -func (m *Span) GetOperationName() string { - if m != nil { - return m.OperationName - } - return "" -} - -func (m *Span) GetCaller() string { - if m != nil { - return m.Caller - } - return "" -} - -func (m *Span) GetTraceId() uint64 { - if m != nil { - return m.TraceId - } - return 0 -} - -func (m *Span) GetSpanId() uint64 { - if m != nil { - return m.SpanId - } - return 0 -} - -func (m *Span) GetParentId() uint64 { - if m != nil { - return m.ParentId - } - return 0 -} - -func (m *Span) GetLevel() int32 { - if m != nil { - return m.Level - } - return 0 -} - -func (m *Span) GetStartAt() int64 { - if m != nil { - return m.StartAt - } - return 0 -} - -func (m *Span) GetFinishAt() int64 { - if m != nil { - return m.FinishAt - } - return 0 -} - -func (m *Span) GetSamplingProbability() float32 { - if m != nil { - return m.SamplingProbability - } - return 0 -} - -func (m *Span) GetEnv() string { - if m != nil { - return m.Env - } - return "" -} - -func (m *Span) GetStartTime() *timestamp.Timestamp { - if m != nil { - return m.StartTime - } - return nil -} - -func (m *Span) GetDuration() *duration.Duration { - if m != nil { - return m.Duration - } - return nil -} - -func (m *Span) GetReferences() []*SpanRef { - if m != nil { - return m.References - } - return nil -} - -func (m *Span) GetTags() []*Tag { - if m != nil { - return m.Tags - } - return nil -} - -func (m *Span) GetLogs() []*Log { - if m != nil { - return m.Logs - } - return nil -} - -func init() { - proto.RegisterType((*Tag)(nil), "dapper.trace.Tag") - proto.RegisterType((*Field)(nil), "dapper.trace.Field") - proto.RegisterType((*Log)(nil), "dapper.trace.Log") - proto.RegisterType((*SpanRef)(nil), "dapper.trace.SpanRef") - proto.RegisterType((*Span)(nil), "dapper.trace.Span") - proto.RegisterEnum("dapper.trace.Tag_Kind", Tag_Kind_name, Tag_Kind_value) - proto.RegisterEnum("dapper.trace.Log_Kind", Log_Kind_name, Log_Kind_value) - proto.RegisterEnum("dapper.trace.SpanRef_RefType", SpanRef_RefType_name, SpanRef_RefType_value) -} - -func init() { proto.RegisterFile("proto/span.proto", fileDescriptor_span_68a8dae26ef502a2) } - -var fileDescriptor_span_68a8dae26ef502a2 = []byte{ - // 669 bytes of a gzipped FileDescriptorProto - 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0x9c, 0x94, 0xdd, 0x6e, 0xd3, 0x4a, - 0x10, 0xc7, 0xeb, 0xd8, 0x89, 0x9d, 0x49, 0x4e, 0xe5, 0xb3, 0xfd, 0x38, 0xdb, 0x1e, 0x3e, 0x4c, - 0xa4, 0x4a, 0x06, 0x24, 0x07, 0x82, 0x2a, 0xc1, 0x65, 0x4b, 0x15, 0x88, 0x30, 0x0d, 0xda, 0x46, - 0x42, 0xe2, 0x26, 0xda, 0x24, 0x63, 0x63, 0xd5, 0xb1, 0x2d, 0x7b, 0x1b, 0x29, 0xcf, 0xc0, 0x5b, - 0xf0, 0x50, 0xdc, 0xf1, 0x2e, 0x68, 0xd7, 0x4e, 0x9a, 0xd2, 0x22, 0x04, 0x77, 0x3b, 0xf3, 0xff, - 0xed, 0xce, 0xcc, 0xfa, 0xbf, 0x06, 0x3b, 0xcb, 0x53, 0x91, 0x76, 0x8b, 0x8c, 0x27, 0x9e, 0x5a, - 0x92, 0xf6, 0x8c, 0x67, 0x19, 0xe6, 0x9e, 0xc8, 0xf9, 0x14, 0x0f, 0x1f, 0x86, 0x69, 0x1a, 0xc6, - 0xd8, 0x55, 0xda, 0xe4, 0x2a, 0xe8, 0x8a, 0x68, 0x8e, 0x85, 0xe0, 0xf3, 0xac, 0xc4, 0x0f, 0x1f, - 0xfc, 0x0c, 0xcc, 0xae, 0x72, 0x2e, 0xa2, 0xb4, 0x3a, 0xae, 0xf3, 0x45, 0x03, 0x7d, 0xc4, 0x43, - 0x62, 0x83, 0x7e, 0x89, 0x4b, 0xaa, 0x39, 0x9a, 0xdb, 0x64, 0x72, 0x49, 0x9e, 0x80, 0x71, 0x19, - 0x25, 0x33, 0x5a, 0x73, 0x34, 0x77, 0xbb, 0xb7, 0xef, 0x6d, 0xd6, 0xf5, 0x46, 0x3c, 0xf4, 0xde, - 0x45, 0xc9, 0x8c, 0x29, 0x86, 0xec, 0x42, 0x7d, 0xc1, 0xe3, 0x2b, 0xa4, 0xba, 0xa3, 0xb9, 0x6d, - 0x56, 0x06, 0x9d, 0x67, 0x60, 0x48, 0x86, 0x00, 0x34, 0x2e, 0x46, 0x6c, 0x70, 0xfe, 0xc6, 0xde, - 0x22, 0x26, 0xe8, 0x83, 0xf3, 0x91, 0xad, 0x11, 0x0b, 0x8c, 0xd3, 0xe1, 0xd0, 0xb7, 0x6b, 0xa4, - 0x09, 0xf5, 0xbe, 0x3f, 0x3c, 0x19, 0xd9, 0x7a, 0xa7, 0x0b, 0xf5, 0x7e, 0x84, 0xf1, 0xec, 0x8e, - 0x76, 0xd6, 0x25, 0x6a, 0x9b, 0x25, 0xbe, 0x69, 0xa0, 0xfb, 0xe9, 0x1f, 0xb7, 0xef, 0xa7, 0xbf, - 0x6f, 0x9f, 0xdc, 0x83, 0xe6, 0xfa, 0x36, 0xa9, 0xe1, 0x68, 0xae, 0xce, 0xae, 0x13, 0xe4, 0x29, - 0x34, 0x02, 0xd9, 0x6a, 0x41, 0xeb, 0x8e, 0xee, 0xb6, 0x7a, 0x3b, 0x37, 0x2b, 0xa8, 0x31, 0x58, - 0x85, 0xfc, 0xc5, 0x4d, 0x7c, 0xd5, 0xc0, 0xbc, 0xc8, 0x78, 0xc2, 0x30, 0x20, 0x2f, 0xc1, 0xca, - 0x31, 0x18, 0x8b, 0x65, 0x86, 0x6a, 0xc2, 0xed, 0xde, 0xfd, 0x9b, 0xc5, 0x2a, 0xd0, 0x63, 0x18, - 0x8c, 0x96, 0x19, 0x32, 0x33, 0x2f, 0x17, 0xe4, 0x00, 0x2c, 0x45, 0x8c, 0xa3, 0xf2, 0x22, 0x0c, - 0x66, 0xaa, 0x78, 0x30, 0x23, 0xff, 0x81, 0x29, 0x5d, 0x25, 0x15, 0x5d, 0x29, 0x0d, 0x19, 0x0e, - 0x66, 0x9d, 0xc7, 0x60, 0x56, 0xe7, 0x90, 0x36, 0x58, 0xaf, 0xdf, 0x0e, 0xfc, 0xb3, 0xf1, 0xb0, - 0x6f, 0x6f, 0x11, 0x1b, 0xda, 0xfd, 0xa1, 0xef, 0x0f, 0x3f, 0x5e, 0x8c, 0xfb, 0x6c, 0xf8, 0xde, - 0xd6, 0x3a, 0xdf, 0x0d, 0x30, 0x64, 0x6d, 0x42, 0xc1, 0x5c, 0x60, 0x5e, 0x44, 0x69, 0x42, 0xa7, - 0x8e, 0xe6, 0xd6, 0xd9, 0x2a, 0x24, 0x8f, 0xa0, 0x5d, 0x60, 0xbe, 0x88, 0xa6, 0x38, 0x4e, 0xf8, - 0x1c, 0xab, 0x2f, 0xd4, 0xaa, 0x72, 0xe7, 0x7c, 0x8e, 0xe4, 0x08, 0xb6, 0xd3, 0x0c, 0x4b, 0x57, - 0x96, 0x50, 0x4d, 0x41, 0xff, 0xac, 0xb3, 0x0a, 0xdb, 0x87, 0xc6, 0x94, 0xc7, 0x31, 0xe6, 0xaa, - 0xdf, 0x26, 0xab, 0xa2, 0x1b, 0x33, 0x1a, 0xbf, 0x9c, 0xb1, 0xbe, 0x39, 0x23, 0xf9, 0x1f, 0x9a, - 0x19, 0xcf, 0x31, 0x11, 0x52, 0x6a, 0x28, 0xc9, 0x2a, 0x13, 0x03, 0xe5, 0x86, 0x18, 0x17, 0x18, - 0x53, 0x53, 0x8d, 0x52, 0x06, 0xb2, 0x4c, 0x21, 0x78, 0x2e, 0xc6, 0x5c, 0x50, 0x4b, 0x99, 0xc1, - 0x54, 0xf1, 0x89, 0x90, 0xa7, 0x05, 0x51, 0x12, 0x15, 0x9f, 0xa5, 0xd6, 0x54, 0x9a, 0x55, 0x26, - 0x4e, 0x04, 0x79, 0x0e, 0xbb, 0x05, 0x9f, 0x67, 0x71, 0x94, 0x84, 0xe3, 0x2c, 0x4f, 0x27, 0x7c, - 0x12, 0xc5, 0x91, 0x58, 0x52, 0x70, 0x34, 0xb7, 0xc6, 0x76, 0x56, 0xda, 0x87, 0x6b, 0x49, 0x9a, - 0x19, 0x93, 0x05, 0xdd, 0x29, 0xcd, 0x8c, 0xc9, 0x82, 0xbc, 0x02, 0x28, 0x8b, 0x4b, 0xff, 0xd1, - 0x5d, 0x47, 0x73, 0x5b, 0xbd, 0x43, 0xaf, 0x7c, 0xda, 0xde, 0xea, 0x69, 0x7b, 0xa3, 0x95, 0x39, - 0x59, 0x53, 0xd1, 0x32, 0x26, 0xc7, 0x60, 0xad, 0x9e, 0x3c, 0xdd, 0x53, 0x1b, 0x0f, 0x6e, 0x6d, - 0x3c, 0xab, 0x00, 0xb6, 0x46, 0xc9, 0x31, 0x40, 0x8e, 0x01, 0xe6, 0x98, 0x4c, 0xb1, 0xa0, 0xfb, - 0xca, 0xe2, 0x7b, 0x77, 0xba, 0x8e, 0x6d, 0x80, 0xe4, 0x08, 0x0c, 0xc1, 0xc3, 0x82, 0xb6, 0xd4, - 0x86, 0x7f, 0x6f, 0xfd, 0x34, 0x98, 0x92, 0x25, 0x16, 0xa7, 0x61, 0x41, 0xdb, 0x77, 0x61, 0x7e, - 0x1a, 0x32, 0x25, 0x9f, 0xc2, 0x27, 0x4b, 0xf5, 0x18, 0x62, 0x32, 0x69, 0xa8, 0xd5, 0x8b, 0x1f, - 0x01, 0x00, 0x00, 0xff, 0xff, 0xfe, 0x7b, 0x57, 0x93, 0x12, 0x05, 0x00, 0x00, -} diff --git a/pkg/net/trace/proto/span.proto b/pkg/net/trace/proto/span.proto deleted file mode 100644 index 6e9e2ed76..000000000 --- a/pkg/net/trace/proto/span.proto +++ /dev/null @@ -1,77 +0,0 @@ -syntax = "proto3"; -package dapper.trace; - -import "google/protobuf/timestamp.proto"; -import "google/protobuf/duration.proto"; - -option go_package = "protogen"; - -message Tag { - enum Kind { - STRING = 0; - INT = 1; - BOOL = 2; - FLOAT = 3; - } - string key = 1; - Kind kind = 2; - bytes value = 3; -} - -message Field { - string key = 1; - bytes value = 2; -} - -message Log { - // Deprecated: Kind no long use - enum Kind { - STRING = 0; - INT = 1; - BOOL = 2; - FLOAT = 3; - } - string key = 1; - // Deprecated: Kind no long use - Kind kind = 2; - // Deprecated: Value no long use - bytes value = 3; - int64 timestamp = 4; - repeated Field fields = 5; -} - -// SpanRef describes causal relationship of the current span to another span (e.g. 'child-of') -message SpanRef { - enum RefType { - CHILD_OF = 0; - FOLLOWS_FROM = 1; - } - RefType ref_type = 1; - uint64 trace_id = 2; - uint64 span_id = 3; -} - -// Span represents a named unit of work performed by a service. -message Span { - int32 version = 99; - string service_name = 1; - string operation_name = 2; - // Deprecated: caller no long required - string caller = 3; - uint64 trace_id = 4; - uint64 span_id = 5; - uint64 parent_id = 6; - // Deprecated: level no long required - int32 level = 7; - // Deprecated: use start_time instead instead of start_at - int64 start_at = 8; - // Deprecated: use duration instead instead of finish_at - int64 finish_at = 9; - float sampling_probability = 10; - string env = 19; - google.protobuf.Timestamp start_time = 20; - google.protobuf.Duration duration = 21; - repeated SpanRef references = 22; - repeated Tag tags = 11; - repeated Log logs = 12; -} diff --git a/pkg/net/trace/report.go b/pkg/net/trace/report.go deleted file mode 100644 index 6bce7ea28..000000000 --- a/pkg/net/trace/report.go +++ /dev/null @@ -1,138 +0,0 @@ -package trace - -import ( - "fmt" - "net" - "os" - "sync" - "time" -) - -const ( - // MaxPackageSize . - _maxPackageSize = 1024 * 32 - // safe udp package size // MaxPackageSize = 508 _dataChSize = 4096) - // max memory usage 1024 * 32 * 4096 -> 128MB - _dataChSize = 4096 - _defaultWriteChannalTimeout = 50 * time.Millisecond - _defaultWriteTimeout = 200 * time.Millisecond -) - -// reporter trace reporter. -type reporter interface { - WriteSpan(sp *Span) error - Close() error -} - -// newReport with network address -func newReport(network, address string, timeout time.Duration, protocolVersion int32) reporter { - if timeout == 0 { - timeout = _defaultWriteTimeout - } - report := &connReport{ - network: network, - address: address, - dataCh: make(chan []byte, _dataChSize), - done: make(chan struct{}), - timeout: timeout, - version: protocolVersion, - } - go report.daemon() - return report -} - -type connReport struct { - version int32 - rmx sync.RWMutex - closed bool - - network, address string - - dataCh chan []byte - - conn net.Conn - - done chan struct{} - - timeout time.Duration -} - -func (c *connReport) daemon() { - for b := range c.dataCh { - c.send(b) - } - c.done <- struct{}{} -} - -func (c *connReport) WriteSpan(sp *Span) error { - data, err := marshalSpan(sp, c.version) - if err != nil { - return err - } - return c.writePackage(data) -} - -func (c *connReport) writePackage(data []byte) error { - c.rmx.RLock() - defer c.rmx.RUnlock() - if c.closed { - return fmt.Errorf("report already closed") - } - if len(data) > _maxPackageSize { - return fmt.Errorf("package too large length %d > %d", len(data), _maxPackageSize) - } - select { - case c.dataCh <- data: - return nil - case <-time.After(_defaultWriteChannalTimeout): - return fmt.Errorf("write to data channel timeout") - } -} - -func (c *connReport) Close() error { - c.rmx.Lock() - c.closed = true - c.rmx.Unlock() - - t := time.NewTimer(time.Second) - close(c.dataCh) - select { - case <-t.C: - c.closeConn() - return fmt.Errorf("close report timeout force close") - case <-c.done: - return c.closeConn() - } -} - -func (c *connReport) send(data []byte) { - if c.conn == nil { - if err := c.reconnect(); err != nil { - c.Errorf("connect error: %s retry after second", err) - time.Sleep(time.Second) - return - } - } - c.conn.SetWriteDeadline(time.Now().Add(100 * time.Microsecond)) - if _, err := c.conn.Write(data); err != nil { - c.Errorf("write to conn error: %s, close connect", err) - c.conn.Close() - c.conn = nil - } -} - -func (c *connReport) reconnect() (err error) { - c.conn, err = net.DialTimeout(c.network, c.address, c.timeout) - return -} - -func (c *connReport) closeConn() error { - if c.conn != nil { - return c.conn.Close() - } - return nil -} - -func (c *connReport) Errorf(format string, args ...interface{}) { - fmt.Fprintf(os.Stderr, format+"\n", args...) -} diff --git a/pkg/net/trace/report_test.go b/pkg/net/trace/report_test.go deleted file mode 100644 index 4d0652921..000000000 --- a/pkg/net/trace/report_test.go +++ /dev/null @@ -1,88 +0,0 @@ -package trace - -import ( - "bytes" - "io" - "log" - "net" - "os" - "testing" - - "github.com/stretchr/testify/assert" -) - -func newServer(w io.Writer, network, address string) (func() error, error) { - lis, err := net.Listen(network, address) - if err != nil { - return nil, err - } - done := make(chan struct{}) - go func() { - conn, err := lis.Accept() - if err != nil { - lis.Close() - log.Fatal(err) - } - io.Copy(w, conn) - conn.Close() - done <- struct{}{} - }() - return func() error { - <-done - return lis.Close() - }, nil -} - -func TestReportTCP(t *testing.T) { - buf := &bytes.Buffer{} - cancel, err := newServer(buf, "tcp", "127.0.0.1:6077") - if err != nil { - t.Fatal(err) - } - report := newReport("tcp", "127.0.0.1:6077", 0, 0).(*connReport) - data := []byte("hello, world") - report.writePackage(data) - if err := report.Close(); err != nil { - t.Error(err) - } - cancel() - assert.Equal(t, data, buf.Bytes(), "receive data") -} - -func newUnixgramServer(w io.Writer, address string) (func() error, error) { - conn, err := net.ListenPacket("unixgram", address) - if err != nil { - return nil, err - } - done := make(chan struct{}) - go func() { - p := make([]byte, 4096) - n, _, err := conn.ReadFrom(p) - if err != nil { - log.Fatal(err) - } - w.Write(p[:n]) - done <- struct{}{} - }() - return func() error { - <-done - return conn.Close() - }, nil -} - -func TestReportUnixgram(t *testing.T) { - os.Remove("/tmp/trace.sock") - buf := &bytes.Buffer{} - cancel, err := newUnixgramServer(buf, "/tmp/trace.sock") - if err != nil { - t.Fatal(err) - } - report := newReport("unixgram", "/tmp/trace.sock", 0, 0).(*connReport) - data := []byte("hello, world") - report.writePackage(data) - if err := report.Close(); err != nil { - t.Error(err) - } - cancel() - assert.Equal(t, data, buf.Bytes(), "receive data") -} diff --git a/pkg/net/trace/sample.go b/pkg/net/trace/sample.go deleted file mode 100644 index 6a7aa92fc..000000000 --- a/pkg/net/trace/sample.go +++ /dev/null @@ -1,67 +0,0 @@ -package trace - -import ( - "math/rand" - "sync/atomic" - "time" -) - -const ( - slotLength = 2048 -) - -var ignoreds = []string{"/metrics", "/ping"} // NOTE: add YOUR URL PATH that want ignore - -func init() { - rand.Seed(time.Now().UnixNano()) -} - -func oneAtTimeHash(s string) (hash uint32) { - b := []byte(s) - for i := range b { - hash += uint32(b[i]) - hash += hash << 10 - hash ^= hash >> 6 - } - hash += hash << 3 - hash ^= hash >> 11 - hash += hash << 15 - return -} - -// sampler decides whether a new trace should be sampled or not. -type sampler interface { - IsSampled(traceID uint64, operationName string) (bool, float32) - Close() error -} - -type probabilitySampling struct { - probability float32 - slot [slotLength]int64 -} - -func (p *probabilitySampling) IsSampled(traceID uint64, operationName string) (bool, float32) { - for _, ignored := range ignoreds { - if operationName == ignored { - return false, 0 - } - } - now := time.Now().Unix() - idx := oneAtTimeHash(operationName) % slotLength - old := atomic.LoadInt64(&p.slot[idx]) - if old != now { - atomic.SwapInt64(&p.slot[idx], now) - return true, 1 - } - return rand.Float32() < float32(p.probability), float32(p.probability) -} - -func (p *probabilitySampling) Close() error { return nil } - -// newSampler new probability sampler -func newSampler(probability float32) sampler { - if probability <= 0 || probability > 1 { - panic("probability P ∈ (0, 1]") - } - return &probabilitySampling{probability: probability} -} diff --git a/pkg/net/trace/sample_test.go b/pkg/net/trace/sample_test.go deleted file mode 100644 index 329ec5f16..000000000 --- a/pkg/net/trace/sample_test.go +++ /dev/null @@ -1,35 +0,0 @@ -package trace - -import ( - "testing" -) - -func TestProbabilitySampling(t *testing.T) { - sampler := newSampler(0.001) - t.Run("test one operationName", func(t *testing.T) { - sampled, probability := sampler.IsSampled(0, "test123") - if !sampled || probability != 1 { - t.Errorf("expect sampled and probability == 1 get: %v %f", sampled, probability) - } - }) - t.Run("test probability", func(t *testing.T) { - sampler.IsSampled(0, "test_opt_2") - count := 0 - for i := 0; i < 100000; i++ { - sampled, _ := sampler.IsSampled(0, "test_opt_2") - if sampled { - count++ - } - } - if count < 60 || count > 150 { - t.Errorf("expect count between 60~150 get %d", count) - } - }) -} - -func BenchmarkProbabilitySampling(b *testing.B) { - sampler := newSampler(0.001) - for i := 0; i < b.N; i++ { - sampler.IsSampled(0, "test_opt_xxx") - } -} diff --git a/pkg/net/trace/span.go b/pkg/net/trace/span.go deleted file mode 100644 index fd6699abd..000000000 --- a/pkg/net/trace/span.go +++ /dev/null @@ -1,141 +0,0 @@ -package trace - -import ( - "fmt" - "time" - - protogen "github.com/go-kratos/kratos/pkg/net/trace/proto" -) - -const ( - _maxChilds = 1024 - _maxTags = 128 - _maxLogs = 256 -) - -var _ Trace = &Span{} - -// Span is a trace span. -type Span struct { - dapper *dapper - context spanContext - operationName string - startTime time.Time - duration time.Duration - tags []Tag - logs []*protogen.Log - childs int -} - -func (s *Span) ServiceName() string { - return s.dapper.serviceName -} - -func (s *Span) OperationName() string { - return s.operationName -} - -func (s *Span) StartTime() time.Time { - return s.startTime -} - -func (s *Span) Duration() time.Duration { - return s.duration -} - -func (s *Span) TraceID() string { - return s.context.String() -} - -func (s *Span) Context() spanContext { - return s.context -} - -func (s *Span) Tags() []Tag { - return s.tags -} - -func (s *Span) Logs() []*protogen.Log { - return s.logs -} - -func (s *Span) Fork(serviceName, operationName string) Trace { - if s.childs > _maxChilds { - // if child span more than max childs set return noopspan - return noopspan{} - } - s.childs++ - // 为了兼容临时为 New 的 Span 设置 span.kind - return s.dapper.newSpanWithContext(operationName, s.context).SetTag(TagString(TagSpanKind, "client")) -} - -func (s *Span) Follow(serviceName, operationName string) Trace { - return s.Fork(serviceName, operationName).SetTag(TagString(TagSpanKind, "producer")) -} - -func (s *Span) Finish(perr *error) { - s.duration = time.Since(s.startTime) - if perr != nil && *perr != nil { - err := *perr - s.SetTag(TagBool(TagError, true)) - s.SetLog(Log(LogMessage, err.Error())) - if err, ok := err.(stackTracer); ok { - s.SetLog(Log(LogStack, fmt.Sprintf("%+v", err.StackTrace()))) - } - } - s.dapper.report(s) -} - -func (s *Span) SetTag(tags ...Tag) Trace { - if !s.context.isSampled() && !s.context.isDebug() { - return s - } - if len(s.tags) < _maxTags { - s.tags = append(s.tags, tags...) - } - if len(s.tags) == _maxTags { - s.tags = append(s.tags, Tag{Key: "trace.error", Value: "too many tags"}) - } - return s -} - -// LogFields is an efficient and type-checked way to record key:value -// NOTE current unsupport -func (s *Span) SetLog(logs ...LogField) Trace { - if !s.context.isSampled() && !s.context.isDebug() { - return s - } - if len(s.logs) < _maxLogs { - s.setLog(logs...) - } - if len(s.logs) == _maxLogs { - s.setLog(LogField{Key: "trace.error", Value: "too many logs"}) - } - return s -} - -func (s *Span) setLog(logs ...LogField) Trace { - protoLog := &protogen.Log{ - Timestamp: time.Now().UnixNano(), - Fields: make([]*protogen.Field, len(logs)), - } - for i := range logs { - protoLog.Fields[i] = &protogen.Field{Key: logs[i].Key, Value: []byte(logs[i].Value)} - } - s.logs = append(s.logs, protoLog) - return s -} - -// Visit visits the k-v pair in trace, calling fn for each. -func (s *Span) Visit(fn func(k, v string)) { - fn(KratosTraceID, s.context.String()) -} - -// SetTitle reset trace title -func (s *Span) SetTitle(operationName string) { - s.operationName = operationName -} - -func (s *Span) String() string { - return s.context.String() -} diff --git a/pkg/net/trace/span_test.go b/pkg/net/trace/span_test.go deleted file mode 100644 index 8d6b92085..000000000 --- a/pkg/net/trace/span_test.go +++ /dev/null @@ -1,108 +0,0 @@ -package trace - -import ( - "fmt" - "strconv" - "testing" - "time" - - "github.com/pkg/errors" - "github.com/stretchr/testify/assert" -) - -func TestSpan(t *testing.T) { - report := &mockReport{} - t1 := NewTracer("service1", report, true) - t.Run("test span string", func(t *testing.T) { - sp1 := t1.New("testfinish").(*Span) - assert.NotEmpty(t, fmt.Sprint(sp1)) - }) - t.Run("test fork", func(t *testing.T) { - sp1 := t1.New("testfork").(*Span) - sp2 := sp1.Fork("xxx", "opt_2").(*Span) - assert.Equal(t, sp1.context.TraceID, sp2.context.TraceID) - assert.Equal(t, sp1.context.SpanID, sp2.context.ParentID) - t.Run("test max fork", func(t *testing.T) { - sp3 := sp2.Fork("xx", "xxx") - for i := 0; i < 100; i++ { - sp3 = sp3.Fork("", "xxx") - } - assert.Equal(t, noopspan{}, sp3) - }) - t.Run("test max childs", func(t *testing.T) { - sp3 := sp2.Fork("xx", "xxx") - for i := 0; i < 4096; i++ { - sp3.Fork("", "xxx") - } - assert.Equal(t, noopspan{}, sp3.Fork("xx", "xx")) - }) - }) - t.Run("test finish", func(t *testing.T) { - t.Run("test finish ok", func(t *testing.T) { - sp1 := t1.New("testfinish").(*Span) - time.Sleep(time.Millisecond) - sp1.Finish(nil) - assert.True(t, sp1.startTime.Unix() > 0) - assert.True(t, sp1.duration > time.Microsecond) - }) - t.Run("test finish error", func(t *testing.T) { - sp1 := t1.New("testfinish").(*Span) - time.Sleep(time.Millisecond) - err := fmt.Errorf("🍻") - sp1.Finish(&err) - assert.True(t, sp1.startTime.Unix() > 0) - assert.True(t, sp1.duration > time.Microsecond) - errorTag := false - for _, tag := range sp1.tags { - if tag.Key == TagError && tag.Value != nil { - errorTag = true - } - } - assert.True(t, errorTag) - messageLog := false - for _, log := range sp1.logs { - assert.True(t, log.Timestamp != 0) - for _, field := range log.Fields { - if field.Key == LogMessage && len(field.Value) != 0 { - messageLog = true - } - } - } - assert.True(t, messageLog) - }) - t.Run("test finish error stack", func(t *testing.T) { - sp1 := t1.New("testfinish").(*Span) - time.Sleep(time.Millisecond) - err := fmt.Errorf("🍻") - err = errors.WithStack(err) - sp1.Finish(&err) - ok := false - for _, log := range sp1.logs { - for _, field := range log.Fields { - if field.Key == LogStack && len(field.Value) != 0 { - ok = true - } - } - } - assert.True(t, ok, "LogStack set") - }) - t.Run("test too many tags", func(t *testing.T) { - sp1 := t1.New("testfinish").(*Span) - for i := 0; i < 1024; i++ { - sp1.SetTag(Tag{Key: strconv.Itoa(i), Value: "hello"}) - } - assert.Len(t, sp1.tags, _maxTags+1) - assert.Equal(t, sp1.tags[_maxTags].Key, "trace.error") - assert.Equal(t, sp1.tags[_maxTags].Value, "too many tags") - }) - t.Run("test too many logs", func(t *testing.T) { - sp1 := t1.New("testfinish").(*Span) - for i := 0; i < 1024; i++ { - sp1.SetLog(LogField{Key: strconv.Itoa(i), Value: "hello"}) - } - assert.Len(t, sp1.logs, _maxLogs+1) - assert.Equal(t, sp1.logs[_maxLogs].Fields[0].Key, "trace.error") - assert.Equal(t, sp1.logs[_maxLogs].Fields[0].Value, []byte("too many logs")) - }) - }) -} diff --git a/pkg/net/trace/tag.go b/pkg/net/trace/tag.go deleted file mode 100644 index 3c7329842..000000000 --- a/pkg/net/trace/tag.go +++ /dev/null @@ -1,182 +0,0 @@ -package trace - -// Standard Span tags https://github.com/opentracing/specification/blob/master/semantic_conventions.md#span-tags-table -const ( - // The software package, framework, library, or module that generated the associated Span. - // E.g., "grpc", "django", "JDBI". - // type string - TagComponent = "component" - - // Database instance name. - // E.g., In java, if the jdbc.url="jdbc:mysql://127.0.0.1:3306/customers", the instance name is "customers". - // type string - TagDBInstance = "db.instance" - - // A database statement for the given database type. - // E.g., for db.type="sql", "SELECT * FROM wuser_table"; for db.type="redis", "SET mykey 'WuValue'". - TagDBStatement = "db.statement" - - // Database type. For any SQL database, "sql". For others, the lower-case database category, - // e.g. "cassandra", "hbase", or "redis". - // type string - TagDBType = "db.type" - - // Username for accessing database. E.g., "readonly_user" or "reporting_user" - // type string - TagDBUser = "db.user" - - // true if and only if the application considers the operation represented by the Span to have failed - // type bool - TagError = "error" - - // HTTP method of the request for the associated Span. E.g., "GET", "POST" - // type string - TagHTTPMethod = "http.method" - - // HTTP response status code for the associated Span. E.g., 200, 503, 404 - // type integer - TagHTTPStatusCode = "http.status_code" - - // URL of the request being handled in this segment of the trace, in standard URI format. - // E.g., "https://domain.net/path/to?resource=here" - // type string - TagHTTPURL = "http.url" - - // An address at which messages can be exchanged. - // E.g. A Kafka record has an associated "topic name" that can be extracted by the instrumented producer or consumer and stored using this tag. - // type string - TagMessageBusDestination = "message_bus.destination" - - // Remote "address", suitable for use in a networking client library. - // This may be a "ip:port", a bare "hostname", a FQDN, or even a JDBC substring like "mysql://prod-db:3306" - // type string - TagPeerAddress = "peer.address" - - // Remote hostname. E.g., "opentracing.io", "internal.dns.name" - // type string - TagPeerHostname = "peer.hostname" - - // Remote IPv4 address as a .-separated tuple. E.g., "127.0.0.1" - // type string - TagPeerIPv4 = "peer.ipv4" - - // Remote IPv6 address as a string of colon-separated 4-char hex tuples. - // E.g., "2001:0db8:85a3:0000:0000:8a2e:0370:7334" - // type string - TagPeerIPv6 = "peer.ipv6" - - // Remote port. E.g., 80 - // type integer - TagPeerPort = "peer.port" - - // Remote service name (for some unspecified definition of "service"). - // E.g., "elasticsearch", "a_custom_microservice", "memcache" - // type string - TagPeerService = "peer.service" - - // If greater than 0, a hint to the Tracer to do its best to capture the trace. - // If 0, a hint to the trace to not-capture the trace. If absent, the Tracer should use its default sampling mechanism. - // type string - TagSamplingPriority = "sampling.priority" - - // Either "client" or "server" for the appropriate roles in an RPC, - // and "producer" or "consumer" for the appropriate roles in a messaging scenario. - // type string - TagSpanKind = "span.kind" - - // legacy tag - TagAnnotation = "legacy.annotation" - TagAddress = "legacy.address" - TagComment = "legacy.comment" -) - -// Standard log tags -const ( - // The type or "kind" of an error (only for event="error" logs). E.g., "Exception", "OSError" - // type string - LogErrorKind = "error.kind" - - // For languages that support such a thing (e.g., Java, Python), - // the actual Throwable/Exception/Error object instance itself. - // E.g., A java.lang.UnsupportedOperationException instance, a python exceptions.NameError instance - // type string - LogErrorObject = "error.object" - - // A stable identifier for some notable moment in the lifetime of a Span. For instance, a mutex lock acquisition or release or the sorts of lifetime events in a browser page load described in the Performance.timing specification. E.g., from Zipkin, "cs", "sr", "ss", or "cr". Or, more generally, "initialized" or "timed out". For errors, "error" - // type string - LogEvent = "event" - - // A concise, human-readable, one-line message explaining the event. - // E.g., "Could not connect to backend", "Cache invalidation succeeded" - // type string - LogMessage = "message" - - // A stack trace in platform-conventional format; may or may not pertain to an error. E.g., "File \"example.py\", line 7, in \\ncaller()\nFile \"example.py\", line 5, in caller\ncallee()\nFile \"example.py\", line 2, in callee\nraise Exception(\"Yikes\")\n" - // type string - LogStack = "stack" -) - -// Tag interface -type Tag struct { - Key string - Value interface{} -} - -// TagString new string tag. -func TagString(key string, val string) Tag { - return Tag{Key: key, Value: val} -} - -// TagInt64 new int64 tag. -func TagInt64(key string, val int64) Tag { - return Tag{Key: key, Value: val} -} - -// TagInt new int tag -func TagInt(key string, val int) Tag { - return Tag{Key: key, Value: val} -} - -// TagBool new bool tag -func TagBool(key string, val bool) Tag { - return Tag{Key: key, Value: val} -} - -// TagFloat64 new float64 tag -func TagFloat64(key string, val float64) Tag { - return Tag{Key: key, Value: val} -} - -// TagFloat32 new float64 tag -func TagFloat32(key string, val float32) Tag { - return Tag{Key: key, Value: val} -} - -// String new tag String. -// NOTE: use TagString -func String(key string, val string) Tag { - return TagString(key, val) -} - -// Int new tag Int. -// NOTE: use TagInt -func Int(key string, val int) Tag { - return TagInt(key, val) -} - -// Bool new tagBool -// NOTE: use TagBool -func Bool(key string, val bool) Tag { - return TagBool(key, val) -} - -// Log new log. -func Log(key string, val string) LogField { - return LogField{Key: key, Value: val} -} - -// LogField LogField -type LogField struct { - Key string - Value string -} diff --git a/pkg/net/trace/tag_test.go b/pkg/net/trace/tag_test.go deleted file mode 100644 index 15c5a94c3..000000000 --- a/pkg/net/trace/tag_test.go +++ /dev/null @@ -1 +0,0 @@ -package trace diff --git a/pkg/net/trace/tracer.go b/pkg/net/trace/tracer.go deleted file mode 100644 index 20e71de47..000000000 --- a/pkg/net/trace/tracer.go +++ /dev/null @@ -1,94 +0,0 @@ -package trace - -import ( - "io" -) - -var ( - // global tracer - _tracer Tracer = nooptracer{} -) - -// SetGlobalTracer SetGlobalTracer -func SetGlobalTracer(tracer Tracer) { - _tracer = tracer -} - -// Tracer is a simple, thin interface for Trace creation and propagation. -type Tracer interface { - // New trace instance with given title. - New(operationName string, opts ...Option) Trace - // Inject takes the Trace instance and injects it for - // propagation within `carrier`. The actual type of `carrier` depends on - // the value of `format`. - Inject(t Trace, format interface{}, carrier interface{}) error - // Extract returns a Trace instance given `format` and `carrier`. - // return `ErrTraceNotFound` if trace not found. - Extract(format interface{}, carrier interface{}) (Trace, error) -} - -// New trace instance with given operationName. -func New(operationName string, opts ...Option) Trace { - return _tracer.New(operationName, opts...) -} - -// Inject takes the Trace instance and injects it for -// propagation within `carrier`. The actual type of `carrier` depends on -// the value of `format`. -func Inject(t Trace, format interface{}, carrier interface{}) error { - return _tracer.Inject(t, format, carrier) -} - -// Extract returns a Trace instance given `format` and `carrier`. -// return `ErrTraceNotFound` if trace not found. -func Extract(format interface{}, carrier interface{}) (Trace, error) { - return _tracer.Extract(format, carrier) -} - -// Close trace flush data. -func Close() error { - if closer, ok := _tracer.(io.Closer); ok { - return closer.Close() - } - return nil -} - -// Trace trace common interface. -type Trace interface { - // return current trace id. - TraceID() string - // Fork fork a trace with client trace. - Fork(serviceName, operationName string) Trace - - // Follow - Follow(serviceName, operationName string) Trace - - // Finish when trace finish call it. - Finish(err *error) - - // Scan scan trace into info. - // Deprecated: method Scan is deprecated, use Inject instead of Scan - // Scan(ti *Info) - - // Adds a tag to the trace. - // - // If there is a pre-existing tag set for `key`, it is overwritten. - // - // Tag values can be numeric types, strings, or bools. The behavior of - // other tag value types is undefined at the OpenTracing level. If a - // tracing system does not know how to handle a particular value type, it - // may ignore the tag, but shall not panic. - // NOTE current only support legacy tag: TagAnnotation TagAddress TagComment - // other will be ignore - SetTag(tags ...Tag) Trace - - // LogFields is an efficient and type-checked way to record key:value - // NOTE current unsupport - SetLog(logs ...LogField) Trace - - // Visit visits the k-v pair in trace, calling fn for each. - Visit(fn func(k, v string)) - - // SetTitle reset trace title - SetTitle(title string) -} diff --git a/pkg/net/trace/util.go b/pkg/net/trace/util.go deleted file mode 100644 index 358cb0dc2..000000000 --- a/pkg/net/trace/util.go +++ /dev/null @@ -1,59 +0,0 @@ -package trace - -import ( - "context" - "encoding/binary" - "math/rand" - "time" - - "github.com/go-kratos/kratos/pkg/conf/env" - "github.com/go-kratos/kratos/pkg/net/ip" - - "github.com/pkg/errors" -) - -var _hostHash byte - -func init() { - rand.Seed(time.Now().UnixNano()) - _hostHash = byte(oneAtTimeHash(env.Hostname)) -} - -func extendTag() (tags []Tag) { - tags = append(tags, - TagString("region", env.Region), - TagString("zone", env.Zone), - TagString("hostname", env.Hostname), - TagString("ip", ip.InternalIP()), - ) - return -} - -func genID() uint64 { - var b [8]byte - // i think this code will not survive to 2106-02-07 - binary.BigEndian.PutUint32(b[4:], uint32(time.Now().Unix())>>8) - b[4] = _hostHash - binary.BigEndian.PutUint32(b[:4], uint32(rand.Int31())) - return binary.BigEndian.Uint64(b[:]) -} - -type stackTracer interface { - StackTrace() errors.StackTrace -} - -type ctxKey string - -var _ctxkey ctxKey = "kratos/pkg/net/trace.trace" - -// FromContext returns the trace bound to the context, if any. -func FromContext(ctx context.Context) (t Trace, ok bool) { - t, ok = ctx.Value(_ctxkey).(Trace) - return -} - -// NewContext new a trace context. -// NOTE: This method is not thread safe. -func NewContext(ctx context.Context, t Trace) context.Context { - return context.WithValue(ctx, _ctxkey, t) -} diff --git a/pkg/net/trace/util_test.go b/pkg/net/trace/util_test.go deleted file mode 100644 index 6d98908a1..000000000 --- a/pkg/net/trace/util_test.go +++ /dev/null @@ -1,21 +0,0 @@ -package trace - -import ( - "context" - "testing" - - "github.com/stretchr/testify/assert" -) - -func TestFromContext(t *testing.T) { - report := &mockReport{} - t1 := NewTracer("service1", report, true) - sp1 := t1.New("test123") - ctx := context.Background() - ctx = NewContext(ctx, sp1) - sp2, ok := FromContext(ctx) - if !ok { - t.Fatal("nothing from context") - } - assert.Equal(t, sp1, sp2) -} diff --git a/pkg/net/trace/zipkin/config.go b/pkg/net/trace/zipkin/config.go deleted file mode 100644 index 31d89371b..000000000 --- a/pkg/net/trace/zipkin/config.go +++ /dev/null @@ -1,30 +0,0 @@ -package zipkin - -import ( - "time" - - "github.com/go-kratos/kratos/pkg/conf/env" - "github.com/go-kratos/kratos/pkg/net/trace" - xtime "github.com/go-kratos/kratos/pkg/time" -) - -// Config config. -// url should be the endpoint to send the spans to, e.g. -// http://localhost:9411/api/v2/spans -type Config struct { - Endpoint string `dsn:"endpoint"` - BatchSize int `dsn:"query.batch_size,100"` - Timeout xtime.Duration `dsn:"query.timeout,200ms"` - DisableSample bool `dsn:"query.disable_sample"` -} - -// Init init trace report. -func Init(c *Config) { - if c.BatchSize == 0 { - c.BatchSize = 100 - } - if c.Timeout == 0 { - c.Timeout = xtime.Duration(200 * time.Millisecond) - } - trace.SetGlobalTracer(trace.NewTracer(env.AppID, newReport(c), c.DisableSample)) -} diff --git a/pkg/net/trace/zipkin/zipkin.go b/pkg/net/trace/zipkin/zipkin.go deleted file mode 100644 index 59dcd86f1..000000000 --- a/pkg/net/trace/zipkin/zipkin.go +++ /dev/null @@ -1,99 +0,0 @@ -package zipkin - -import ( - "fmt" - "time" - - protogen "github.com/go-kratos/kratos/pkg/net/trace/proto" - - "github.com/openzipkin/zipkin-go/model" - "github.com/openzipkin/zipkin-go/reporter" - "github.com/openzipkin/zipkin-go/reporter/http" - - "github.com/go-kratos/kratos/pkg/net/trace" -) - -type report struct { - rpt reporter.Reporter -} - -func newReport(c *Config) *report { - return &report{ - rpt: http.NewReporter(c.Endpoint, - http.Timeout(time.Duration(c.Timeout)), - http.BatchSize(c.BatchSize), - ), - } -} - -// WriteSpan write a trace span to queue. -func (r *report) WriteSpan(raw *trace.Span) (err error) { - ctx := raw.Context() - traceID := model.TraceID{Low: ctx.TraceID} - spanID := model.ID(ctx.SpanID) - parentID := model.ID(ctx.ParentID) - tags := raw.Tags() - span := model.SpanModel{ - SpanContext: model.SpanContext{ - TraceID: traceID, - ID: spanID, - ParentID: &parentID, - }, - Name: raw.OperationName(), - Timestamp: raw.StartTime(), - Duration: raw.Duration(), - Tags: make(map[string]string, len(tags)), - } - span.LocalEndpoint = &model.Endpoint{ServiceName: raw.ServiceName()} - for _, tag := range tags { - switch tag.Key { - case trace.TagSpanKind: - switch tag.Value.(string) { - case "client": - span.Kind = model.Client - case "server": - span.Kind = model.Server - case "producer": - span.Kind = model.Producer - case "consumer": - span.Kind = model.Consumer - } - default: - v, ok := tag.Value.(string) - if ok { - span.Tags[tag.Key] = v - } else { - span.Tags[tag.Key] = fmt.Sprint(v) - } - } - } - //log save to zipkin annotation - span.Annotations = r.converLogsToAnnotations(raw.Logs()) - r.rpt.Send(span) - return -} - -func (r *report) converLogsToAnnotations(logs []*protogen.Log) (annotations []model.Annotation) { - annotations = make([]model.Annotation, 0, len(annotations)) - for _, lg := range logs { - annotations = append(annotations, r.converLogToAnnotation(lg)...) - } - return annotations -} -func (r *report) converLogToAnnotation(log *protogen.Log) (annotations []model.Annotation) { - annotations = make([]model.Annotation, 0, len(log.Fields)) - for _, field := range log.Fields { - val := string(field.Value) - annotation := model.Annotation{ - Timestamp: time.Unix(0, log.Timestamp), - Value: field.Key + " : " + val, - } - annotations = append(annotations, annotation) - } - return annotations -} - -// Close close the report. -func (r *report) Close() error { - return r.rpt.Close() -} diff --git a/pkg/net/trace/zipkin/zipkin_test.go b/pkg/net/trace/zipkin/zipkin_test.go deleted file mode 100644 index 394a74a64..000000000 --- a/pkg/net/trace/zipkin/zipkin_test.go +++ /dev/null @@ -1,53 +0,0 @@ -package zipkin - -import ( - "io/ioutil" - "net/http" - "net/http/httptest" - "testing" - "time" - - "github.com/go-kratos/kratos/pkg/net/trace" - xtime "github.com/go-kratos/kratos/pkg/time" -) - -func TestZipkin(t *testing.T) { - ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - if r.Method != "POST" { - t.Errorf("expected 'POST' request, got '%s'", r.Method) - } - - aSpanPayload, err := ioutil.ReadAll(r.Body) - if err != nil { - t.Errorf("unexpected error: %s", err.Error()) - } - - t.Logf("%s\n", aSpanPayload) - })) - defer ts.Close() - - c := &Config{ - Endpoint: ts.URL, - Timeout: xtime.Duration(time.Second * 5), - BatchSize: 100, - } - //c.Endpoint = "http://127.0.0.1:9411/api/v2/spans" - report := newReport(c) - t1 := trace.NewTracer("service1", report, true) - t2 := trace.NewTracer("service2", report, true) - sp1 := t1.New("option_1") - sp2 := sp1.Fork("service3", "opt_client") - sp2.SetLog(trace.Log("log_k", "log_v")) - // inject - header := make(http.Header) - t1.Inject(sp2, trace.HTTPFormat, header) - t.Log(header) - sp3, err := t2.Extract(trace.HTTPFormat, header) - if err != nil { - t.Fatal(err) - } - sp3.Finish(nil) - sp2.Finish(nil) - sp1.Finish(nil) - report.Close() -} diff --git a/pkg/ratelimit/README.md b/pkg/ratelimit/README.md deleted file mode 100644 index 5cc494138..000000000 --- a/pkg/ratelimit/README.md +++ /dev/null @@ -1,14 +0,0 @@ -# rate - -# 项目简介 -BBR 限流 - -# 编译环境 - - -# 依赖包 - - -# 编译执行 - - \ No newline at end of file diff --git a/pkg/ratelimit/bbr/bbr.go b/pkg/ratelimit/bbr/bbr.go deleted file mode 100644 index 738336e9d..000000000 --- a/pkg/ratelimit/bbr/bbr.go +++ /dev/null @@ -1,284 +0,0 @@ -package bbr - -import ( - "context" - "math" - "sync/atomic" - "time" - - "github.com/go-kratos/kratos/pkg/container/group" - "github.com/go-kratos/kratos/pkg/ecode" - "github.com/go-kratos/kratos/pkg/log" - limit "github.com/go-kratos/kratos/pkg/ratelimit" - "github.com/go-kratos/kratos/pkg/stat/metric" - - cpustat "github.com/go-kratos/kratos/pkg/stat/sys/cpu" -) - -var ( - cpu int64 - decay = 0.95 - initTime = time.Now() - defaultConf = &Config{ - Window: time.Second * 10, - WinBucket: 100, - CPUThreshold: 800, - } -) - -type cpuGetter func() int64 - -func init() { - go cpuproc() -} - -// cpu = cpuᵗ⁻¹ * decay + cpuᵗ * (1 - decay) -func cpuproc() { - ticker := time.NewTicker(time.Millisecond * 250) - defer func() { - ticker.Stop() - if err := recover(); err != nil { - log.Error("rate.limit.cpuproc() err(%+v)", err) - go cpuproc() - } - }() - - // EMA algorithm: https://blog.csdn.net/m0_38106113/article/details/81542863 - for range ticker.C { - stat := &cpustat.Stat{} - cpustat.ReadStat(stat) - prevCpu := atomic.LoadInt64(&cpu) - curCpu := int64(float64(prevCpu)*decay + float64(stat.Usage)*(1.0-decay)) - atomic.StoreInt64(&cpu, curCpu) - } -} - -// Stats contains the metrics's snapshot of bbr. -type Stat struct { - Cpu int64 - InFlight int64 - MaxInFlight int64 - MinRt int64 - MaxPass int64 -} - -// BBR implements bbr-like limiter. -// It is inspired by sentinel. -// https://github.com/alibaba/Sentinel/wiki/%E7%B3%BB%E7%BB%9F%E8%87%AA%E9%80%82%E5%BA%94%E9%99%90%E6%B5%81 -type BBR struct { - cpu cpuGetter - passStat metric.RollingCounter - rtStat metric.RollingCounter - inFlight int64 - winBucketPerSec int64 - bucketDuration time.Duration - winSize int - conf *Config - prevDrop atomic.Value - maxPASSCache atomic.Value - minRtCache atomic.Value -} - -// CounterCache is used to cache maxPASS and minRt result. -// Value of current bucket is not counted in real time. -// Cache time is equal to a bucket duration. -type CounterCache struct { - val int64 - time time.Time -} - -// Config contains configs of bbr limiter. -type Config struct { - Enabled bool - Window time.Duration - WinBucket int - Rule string - Debug bool - CPUThreshold int64 -} - -func (l *BBR) maxPASS() int64 { - passCache := l.maxPASSCache.Load() - if passCache != nil { - ps := passCache.(*CounterCache) - if l.timespan(ps.time) < 1 { - return ps.val - } - } - rawMaxPass := int64(l.passStat.Reduce(func(iterator metric.Iterator) float64 { - var result = 1.0 - for i := 1; iterator.Next() && i < l.conf.WinBucket; i++ { - bucket := iterator.Bucket() - count := 0.0 - for _, p := range bucket.Points { - count += p - } - result = math.Max(result, count) - } - return result - })) - if rawMaxPass == 0 { - rawMaxPass = 1 - } - l.maxPASSCache.Store(&CounterCache{ - val: rawMaxPass, - time: time.Now(), - }) - return rawMaxPass -} - -func (l *BBR) timespan(lastTime time.Time) int { - v := int(time.Since(lastTime) / l.bucketDuration) - if v > -1 { - return v - } - return l.winSize -} - -func (l *BBR) minRT() int64 { - rtCache := l.minRtCache.Load() - if rtCache != nil { - rc := rtCache.(*CounterCache) - if l.timespan(rc.time) < 1 { - return rc.val - } - } - rawMinRT := int64(math.Ceil(l.rtStat.Reduce(func(iterator metric.Iterator) float64 { - var result = math.MaxFloat64 - for i := 1; iterator.Next() && i < l.conf.WinBucket; i++ { - bucket := iterator.Bucket() - if len(bucket.Points) == 0 { - continue - } - total := 0.0 - for _, p := range bucket.Points { - total += p - } - avg := total / float64(bucket.Count) - result = math.Min(result, avg) - } - return result - }))) - if rawMinRT <= 0 { - rawMinRT = 1 - } - l.minRtCache.Store(&CounterCache{ - val: rawMinRT, - time: time.Now(), - }) - return rawMinRT -} - -func (l *BBR) maxFlight() int64 { - return int64(math.Floor(float64(l.maxPASS()*l.minRT()*l.winBucketPerSec)/1000.0 + 0.5)) -} - -func (l *BBR) shouldDrop() bool { - if l.cpu() < l.conf.CPUThreshold { - prevDrop, _ := l.prevDrop.Load().(time.Duration) - if prevDrop == 0 { - return false - } - if time.Since(initTime)-prevDrop <= time.Second { - inFlight := atomic.LoadInt64(&l.inFlight) - return inFlight > 1 && inFlight > l.maxFlight() - } - l.prevDrop.Store(time.Duration(0)) - return false - } - inFlight := atomic.LoadInt64(&l.inFlight) - drop := inFlight > 1 && inFlight > l.maxFlight() - if drop { - prevDrop, _ := l.prevDrop.Load().(time.Duration) - if prevDrop != 0 { - return drop - } - l.prevDrop.Store(time.Since(initTime)) - } - return drop -} - -// Stat tasks a snapshot of the bbr limiter. -func (l *BBR) Stat() Stat { - return Stat{ - Cpu: l.cpu(), - InFlight: atomic.LoadInt64(&l.inFlight), - MinRt: l.minRT(), - MaxPass: l.maxPASS(), - MaxInFlight: l.maxFlight(), - } -} - -// Allow checks all inbound traffic. -// Once overload is detected, it raises ecode.LimitExceed error. -func (l *BBR) Allow(ctx context.Context, opts ...limit.AllowOption) (func(info limit.DoneInfo), error) { - allowOpts := limit.DefaultAllowOpts() - for _, opt := range opts { - opt.Apply(&allowOpts) - } - if l.shouldDrop() { - return nil, ecode.LimitExceed - } - atomic.AddInt64(&l.inFlight, 1) - stime := time.Since(initTime) - return func(do limit.DoneInfo) { - rt := int64((time.Since(initTime) - stime) / time.Millisecond) - l.rtStat.Add(rt) - atomic.AddInt64(&l.inFlight, -1) - switch do.Op { - case limit.Success: - l.passStat.Add(1) - return - default: - return - } - }, nil -} - -func newLimiter(conf *Config) limit.Limiter { - if conf == nil { - conf = defaultConf - } - size := conf.WinBucket - bucketDuration := conf.Window / time.Duration(conf.WinBucket) - passStat := metric.NewRollingCounter(metric.RollingCounterOpts{Size: size, BucketDuration: bucketDuration}) - rtStat := metric.NewRollingCounter(metric.RollingCounterOpts{Size: size, BucketDuration: bucketDuration}) - cpu := func() int64 { - return atomic.LoadInt64(&cpu) - } - limiter := &BBR{ - cpu: cpu, - conf: conf, - passStat: passStat, - rtStat: rtStat, - winBucketPerSec: int64(time.Second) / (int64(conf.Window) / int64(conf.WinBucket)), - bucketDuration: bucketDuration, - winSize: conf.WinBucket, - } - return limiter -} - -// Group represents a class of BBRLimiter and forms a namespace in which -// units of BBRLimiter. -type Group struct { - group *group.Group -} - -// NewGroup new a limiter group container, if conf nil use default conf. -func NewGroup(conf *Config) *Group { - if conf == nil { - conf = defaultConf - } - group := group.NewGroup(func() interface{} { - return newLimiter(conf) - }) - return &Group{ - group: group, - } -} - -// Get get a limiter by a specified key, if limiter not exists then make a new one. -func (g *Group) Get(key string) limit.Limiter { - limiter := g.group.Get(key) - return limiter.(limit.Limiter) -} diff --git a/pkg/ratelimit/bbr/bbr_test.go b/pkg/ratelimit/bbr/bbr_test.go deleted file mode 100644 index 93345e5ff..000000000 --- a/pkg/ratelimit/bbr/bbr_test.go +++ /dev/null @@ -1,298 +0,0 @@ -package bbr - -import ( - "context" - "fmt" - "math/rand" - "sync" - "sync/atomic" - "testing" - "time" - - "github.com/stretchr/testify/assert" - - "github.com/go-kratos/kratos/pkg/ratelimit" - "github.com/go-kratos/kratos/pkg/stat/metric" -) - -func confForTest() *Config { - return &Config{ - Window: time.Second, - WinBucket: 10, - CPUThreshold: 800, - } -} - -func warmup(bbr *BBR, count int) { - for i := 0; i < count; i++ { - done, err := bbr.Allow(context.TODO()) - time.Sleep(time.Millisecond * 1) - if err == nil { - done(ratelimit.DoneInfo{Op: ratelimit.Success}) - } - } -} - -func forceAllow(bbr *BBR) { - inflight := bbr.inFlight - bbr.inFlight = bbr.maxPASS() - 1 - done, err := bbr.Allow(context.TODO()) - if err == nil { - done(ratelimit.DoneInfo{Op: ratelimit.Success}) - } - bbr.inFlight = inflight -} - -func TestBBR(t *testing.T) { - cfg := &Config{ - Window: time.Second * 5, - WinBucket: 50, - CPUThreshold: 100, - } - limiter := newLimiter(cfg) - var wg sync.WaitGroup - var drop int64 - for i := 0; i < 100; i++ { - wg.Add(1) - go func() { - defer wg.Done() - for i := 0; i < 300; i++ { - f, err := limiter.Allow(context.TODO()) - if err != nil { - atomic.AddInt64(&drop, 1) - } else { - count := rand.Intn(100) - time.Sleep(time.Millisecond * time.Duration(count)) - f(ratelimit.DoneInfo{Op: ratelimit.Success}) - } - } - }() - } - wg.Wait() - fmt.Println("drop: ", drop) -} - -func TestBBRMaxPass(t *testing.T) { - bucketDuration := time.Millisecond * 100 - bbr := newLimiter(confForTest()).(*BBR) - for i := 1; i <= 10; i++ { - bbr.passStat.Add(int64(i * 100)) - time.Sleep(bucketDuration) - } - assert.Equal(t, int64(1000), bbr.maxPASS()) - - // default max pass is equal to 1. - bbr = newLimiter(confForTest()).(*BBR) - assert.Equal(t, int64(1), bbr.maxPASS()) -} - -func TestBBRMaxPassWithCache(t *testing.T) { - bucketDuration := time.Millisecond * 100 - bbr := newLimiter(confForTest()).(*BBR) - // witch cache, value of latest bucket is not counted instently. - // after a bucket duration time, this bucket will be fullly counted. - for i := 1; i <= 11; i++ { - bbr.passStat.Add(int64(i * 50)) - time.Sleep(bucketDuration / 2) - _ = bbr.maxPASS() - bbr.passStat.Add(int64(i * 50)) - time.Sleep(bucketDuration / 2) - } - bbr.passStat.Add(int64(1)) - assert.Equal(t, int64(1000), bbr.maxPASS()) -} - -func TestBBRMinRt(t *testing.T) { - bucketDuration := time.Millisecond * 100 - bbr := newLimiter(confForTest()).(*BBR) - for i := 0; i < 10; i++ { - for j := i*10 + 1; j <= i*10+10; j++ { - bbr.rtStat.Add(int64(j)) - } - if i != 9 { - time.Sleep(bucketDuration) - } - } - assert.Equal(t, int64(6), bbr.minRT()) - - // default max min rt is equal to maxFloat64. - bucketDuration = time.Millisecond * 100 - bbr = newLimiter(confForTest()).(*BBR) - bbr.rtStat = metric.NewRollingCounter(metric.RollingCounterOpts{Size: 10, BucketDuration: bucketDuration}) - assert.Equal(t, int64(1), bbr.minRT()) -} - -func TestBBRMinRtWithCache(t *testing.T) { - bucketDuration := time.Millisecond * 100 - bbr := newLimiter(confForTest()).(*BBR) - for i := 0; i < 10; i++ { - for j := i*10 + 1; j <= i*10+5; j++ { - bbr.rtStat.Add(int64(j)) - } - if i != 9 { - time.Sleep(bucketDuration / 2) - } - _ = bbr.minRT() - for j := i*10 + 6; j <= i*10+10; j++ { - bbr.rtStat.Add(int64(j)) - } - if i != 9 { - time.Sleep(bucketDuration / 2) - } - } - assert.Equal(t, int64(6), bbr.minRT()) -} - -func TestBBRMaxQps(t *testing.T) { - bbr := newLimiter(confForTest()).(*BBR) - bucketDuration := time.Millisecond * 100 - passStat := metric.NewRollingCounter(metric.RollingCounterOpts{Size: 10, BucketDuration: bucketDuration}) - rtStat := metric.NewRollingCounter(metric.RollingCounterOpts{Size: 10, BucketDuration: bucketDuration}) - for i := 0; i < 10; i++ { - passStat.Add(int64((i + 2) * 100)) - for j := i*10 + 1; j <= i*10+10; j++ { - rtStat.Add(int64(j)) - } - if i != 9 { - time.Sleep(bucketDuration) - } - } - bbr.passStat = passStat - bbr.rtStat = rtStat - assert.Equal(t, int64(60), bbr.maxFlight()) -} - -func TestBBRShouldDrop(t *testing.T) { - var cpu int64 - bbr := newLimiter(confForTest()).(*BBR) - bbr.cpu = func() int64 { - return cpu - } - bucketDuration := time.Millisecond * 100 - passStat := metric.NewRollingCounter(metric.RollingCounterOpts{Size: 10, BucketDuration: bucketDuration}) - rtStat := metric.NewRollingCounter(metric.RollingCounterOpts{Size: 10, BucketDuration: bucketDuration}) - for i := 0; i < 10; i++ { - passStat.Add(int64((i + 1) * 100)) - for j := i*10 + 1; j <= i*10+10; j++ { - rtStat.Add(int64(j)) - } - if i != 9 { - time.Sleep(bucketDuration) - } - } - bbr.passStat = passStat - bbr.rtStat = rtStat - // cpu >= 800, inflight < maxQps - cpu = 800 - bbr.inFlight = 50 - assert.Equal(t, false, bbr.shouldDrop()) - - // cpu >= 800, inflight > maxQps - cpu = 800 - bbr.inFlight = 80 - assert.Equal(t, true, bbr.shouldDrop()) - - // cpu < 800, inflight > maxQps, cold duration - cpu = 700 - bbr.inFlight = 80 - assert.Equal(t, true, bbr.shouldDrop()) - - // cpu < 800, inflight > maxQps - time.Sleep(2 * time.Second) - cpu = 700 - bbr.inFlight = 80 - assert.Equal(t, false, bbr.shouldDrop()) -} - -func TestGroup(t *testing.T) { - cfg := &Config{ - Window: time.Second * 5, - WinBucket: 50, - CPUThreshold: 100, - } - group := NewGroup(cfg) - t.Run("get", func(t *testing.T) { - limiter := group.Get("test") - assert.NotNil(t, limiter) - }) -} - -func BenchmarkBBRAllowUnderLowLoad(b *testing.B) { - bbr := newLimiter(confForTest()).(*BBR) - bbr.cpu = func() int64 { - return 500 - } - b.ResetTimer() - for i := 0; i <= b.N; i++ { - done, err := bbr.Allow(context.TODO()) - if err == nil { - done(ratelimit.DoneInfo{Op: ratelimit.Success}) - } - } -} - -func BenchmarkBBRAllowUnderHighLoad(b *testing.B) { - bbr := newLimiter(confForTest()).(*BBR) - bbr.cpu = func() int64 { - return 900 - } - bbr.inFlight = 1 - b.ResetTimer() - for i := 0; i <= b.N; i++ { - if i%10000 == 0 { - maxFlight := bbr.maxFlight() - if maxFlight != 0 { - bbr.inFlight = rand.Int63n(bbr.maxFlight() * 2) - } - } - done, err := bbr.Allow(context.TODO()) - if err == nil { - done(ratelimit.DoneInfo{Op: ratelimit.Success}) - } - } -} - -func BenchmarkBBRShouldDropUnderLowLoad(b *testing.B) { - bbr := newLimiter(confForTest()).(*BBR) - bbr.cpu = func() int64 { - return 500 - } - warmup(bbr, 10000) - b.ResetTimer() - for i := 0; i <= b.N; i++ { - bbr.shouldDrop() - } -} - -func BenchmarkBBRShouldDropUnderHighLoad(b *testing.B) { - bbr := newLimiter(confForTest()).(*BBR) - bbr.cpu = func() int64 { - return 900 - } - warmup(bbr, 10000) - bbr.inFlight = 1000 - b.ResetTimer() - for i := 0; i <= b.N; i++ { - bbr.shouldDrop() - if i%10000 == 0 { - forceAllow(bbr) - } - } -} - -func BenchmarkBBRShouldDropUnderUnstableLoad(b *testing.B) { - bbr := newLimiter(confForTest()).(*BBR) - bbr.cpu = func() int64 { - return 500 - } - warmup(bbr, 10000) - bbr.prevDrop.Store(time.Since(initTime)) - bbr.inFlight = 1000 - b.ResetTimer() - for i := 0; i <= b.N; i++ { - bbr.shouldDrop() - if i%100000 == 0 { - forceAllow(bbr) - } - } -} diff --git a/pkg/ratelimit/limiter.go b/pkg/ratelimit/limiter.go deleted file mode 100644 index 79c8e8ab1..000000000 --- a/pkg/ratelimit/limiter.go +++ /dev/null @@ -1,40 +0,0 @@ -package ratelimit - -import ( - "context" -) - -// Op operations type. -type Op int - -const ( - // Success opertion type: success - Success Op = iota - // Ignore opertion type: ignore - Ignore - // Drop opertion type: drop - Drop -) - -type allowOptions struct{} - -// AllowOptions allow options. -type AllowOption interface { - Apply(*allowOptions) -} - -// DoneInfo done info. -type DoneInfo struct { - Err error - Op Op -} - -// DefaultAllowOpts returns the default allow options. -func DefaultAllowOpts() allowOptions { - return allowOptions{} -} - -// Limiter limit interface. -type Limiter interface { - Allow(ctx context.Context, opts ...AllowOption) (func(info DoneInfo), error) -} diff --git a/pkg/stat/README.md b/pkg/stat/README.md deleted file mode 100644 index 676067e2f..000000000 --- a/pkg/stat/README.md +++ /dev/null @@ -1,5 +0,0 @@ -# stat - -## 项目简介 - -数据统计、监控采集等 diff --git a/pkg/stat/metric/counter.go b/pkg/stat/metric/counter.go deleted file mode 100644 index 6015899d2..000000000 --- a/pkg/stat/metric/counter.go +++ /dev/null @@ -1,84 +0,0 @@ -package metric - -import ( - "fmt" - "sync/atomic" - - "github.com/prometheus/client_golang/prometheus" -) - -var _ Metric = &counter{} - -// Counter stores a numerical value that only ever goes up. -type Counter interface { - Metric -} - -// CounterOpts is an alias of Opts. -type CounterOpts Opts - -type counter struct { - val int64 -} - -// NewCounter creates a new Counter based on the CounterOpts. -func NewCounter(opts CounterOpts) Counter { - return &counter{} -} - -func (c *counter) Add(val int64) { - if val < 0 { - panic(fmt.Errorf("stat/metric: cannot decrease in negative value. val: %d", val)) - } - atomic.AddInt64(&c.val, val) -} - -func (c *counter) Value() int64 { - return atomic.LoadInt64(&c.val) -} - -// CounterVecOpts is an alias of VectorOpts. -type CounterVecOpts VectorOpts - -// CounterVec counter vec. -type CounterVec interface { - // Inc increments the counter by 1. Use Add to increment it by arbitrary - // non-negative values. - Inc(labels ...string) - // Add adds the given value to the counter. It panics if the value is < - // 0. - Add(v float64, labels ...string) -} - -// counterVec counter vec. -type promCounterVec struct { - counter *prometheus.CounterVec -} - -// NewCounterVec . -func NewCounterVec(cfg *CounterVecOpts) CounterVec { - if cfg == nil { - return nil - } - vec := prometheus.NewCounterVec( - prometheus.CounterOpts{ - Namespace: cfg.Namespace, - Subsystem: cfg.Subsystem, - Name: cfg.Name, - Help: cfg.Help, - }, cfg.Labels) - prometheus.MustRegister(vec) - return &promCounterVec{ - counter: vec, - } -} - -// Inc Inc increments the counter by 1. Use Add to increment it by arbitrary. -func (counter *promCounterVec) Inc(labels ...string) { - counter.counter.WithLabelValues(labels...).Inc() -} - -// Add Inc increments the counter by 1. Use Add to increment it by arbitrary. -func (counter *promCounterVec) Add(v float64, labels ...string) { - counter.counter.WithLabelValues(labels...).Add(v) -} diff --git a/pkg/stat/metric/counter_test.go b/pkg/stat/metric/counter_test.go deleted file mode 100644 index 413a61c94..000000000 --- a/pkg/stat/metric/counter_test.go +++ /dev/null @@ -1,47 +0,0 @@ -package metric - -import ( - "math/rand" - "testing" - - "github.com/stretchr/testify/assert" -) - -func TestCounter(t *testing.T) { - counter := NewCounter(CounterOpts{}) - count := rand.Intn(100) - for i := 0; i < count; i++ { - counter.Add(1) - } - val := counter.Value() - assert.Equal(t, val, int64(count)) -} - -func TestCounterVec(t *testing.T) { - counterVec := NewCounterVec(&CounterVecOpts{ - Namespace: "test", - Subsystem: "test", - Name: "test", - Help: "this is test metrics.", - Labels: []string{"name", "addr"}, - }) - counterVec.Inc("name1", "127.0.0.1") - assert.Panics(t, func() { - NewCounterVec(&CounterVecOpts{ - Namespace: "test", - Subsystem: "test", - Name: "test", - Help: "this is test metrics.", - Labels: []string{"name", "addr"}, - }) - }, "Expected to panic.") - assert.NotPanics(t, func() { - NewCounterVec(&CounterVecOpts{ - Namespace: "test", - Subsystem: "test", - Name: "test2", - Help: "this is test metrics.", - Labels: []string{"name", "addr"}, - }) - }, "Expected normal. no panic.") -} diff --git a/pkg/stat/metric/gauge.go b/pkg/stat/metric/gauge.go deleted file mode 100644 index de6980a49..000000000 --- a/pkg/stat/metric/gauge.go +++ /dev/null @@ -1,94 +0,0 @@ -package metric - -import ( - "sync/atomic" - - "github.com/prometheus/client_golang/prometheus" -) - -var _ Metric = &gauge{} - -// Gauge stores a numerical value that can be add arbitrarily. -type Gauge interface { - Metric - // Sets sets the value to the given number. - Set(int64) -} - -// GaugeOpts is an alias of Opts. -type GaugeOpts Opts - -type gauge struct { - val int64 -} - -// NewGauge creates a new Gauge based on the GaugeOpts. -func NewGauge(opts GaugeOpts) Gauge { - return &gauge{} -} - -func (g *gauge) Add(val int64) { - atomic.AddInt64(&g.val, val) -} - -func (g *gauge) Set(val int64) { - old := atomic.LoadInt64(&g.val) - atomic.CompareAndSwapInt64(&g.val, old, val) -} - -func (g *gauge) Value() int64 { - return atomic.LoadInt64(&g.val) -} - -// GaugeVecOpts is an alias of VectorOpts. -type GaugeVecOpts VectorOpts - -// GaugeVec gauge vec. -type GaugeVec interface { - // Set sets the Gauge to an arbitrary value. - Set(v float64, labels ...string) - // Inc increments the Gauge by 1. Use Add to increment it by arbitrary - // values. - Inc(labels ...string) - // Add adds the given value to the Gauge. (The value can be negative, - // resulting in a decrease of the Gauge.) - Add(v float64, labels ...string) -} - -// gaugeVec gauge vec. -type promGaugeVec struct { - gauge *prometheus.GaugeVec -} - -// NewGaugeVec . -func NewGaugeVec(cfg *GaugeVecOpts) GaugeVec { - if cfg == nil { - return nil - } - vec := prometheus.NewGaugeVec( - prometheus.GaugeOpts{ - Namespace: cfg.Namespace, - Subsystem: cfg.Subsystem, - Name: cfg.Name, - Help: cfg.Help, - }, cfg.Labels) - prometheus.MustRegister(vec) - return &promGaugeVec{ - gauge: vec, - } -} - -// Inc Inc increments the counter by 1. Use Add to increment it by arbitrary. -func (gauge *promGaugeVec) Inc(labels ...string) { - gauge.gauge.WithLabelValues(labels...).Inc() -} - -// Add Inc increments the counter by 1. Use Add to increment it by arbitrary. -func (gauge *promGaugeVec) Add(v float64, labels ...string) { - gauge.gauge.WithLabelValues(labels...).Add(v) -} - -// Set set the given value to the collection. -func (gauge *promGaugeVec) Set(v float64, labels ...string) { - gauge.gauge.WithLabelValues(labels...).Set(v) -} diff --git a/pkg/stat/metric/gauge_test.go b/pkg/stat/metric/gauge_test.go deleted file mode 100644 index b3cb26d22..000000000 --- a/pkg/stat/metric/gauge_test.go +++ /dev/null @@ -1,23 +0,0 @@ -package metric - -import ( - "testing" - - "github.com/stretchr/testify/assert" -) - -func TestGaugeAdd(t *testing.T) { - gauge := NewGauge(GaugeOpts{}) - gauge.Add(100) - gauge.Add(-50) - val := gauge.Value() - assert.Equal(t, val, int64(50)) -} - -func TestGaugeSet(t *testing.T) { - gauge := NewGauge(GaugeOpts{}) - gauge.Add(100) - gauge.Set(50) - val := gauge.Value() - assert.Equal(t, val, int64(50)) -} diff --git a/pkg/stat/metric/histogram.go b/pkg/stat/metric/histogram.go deleted file mode 100644 index 8a7af582c..000000000 --- a/pkg/stat/metric/histogram.go +++ /dev/null @@ -1,50 +0,0 @@ -package metric - -import ( - "github.com/prometheus/client_golang/prometheus" -) - -// HistogramVecOpts is histogram vector opts. -type HistogramVecOpts struct { - Namespace string - Subsystem string - Name string - Help string - Labels []string - Buckets []float64 -} - -// HistogramVec gauge vec. -type HistogramVec interface { - // Observe adds a single observation to the histogram. - Observe(v int64, labels ...string) -} - -// Histogram prom histogram collection. -type promHistogramVec struct { - histogram *prometheus.HistogramVec -} - -// NewHistogramVec new a histogram vec. -func NewHistogramVec(cfg *HistogramVecOpts) HistogramVec { - if cfg == nil { - return nil - } - vec := prometheus.NewHistogramVec( - prometheus.HistogramOpts{ - Namespace: cfg.Namespace, - Subsystem: cfg.Subsystem, - Name: cfg.Name, - Help: cfg.Help, - Buckets: cfg.Buckets, - }, cfg.Labels) - prometheus.MustRegister(vec) - return &promHistogramVec{ - histogram: vec, - } -} - -// Timing adds a single observation to the histogram. -func (histogram *promHistogramVec) Observe(v int64, labels ...string) { - histogram.histogram.WithLabelValues(labels...).Observe(float64(v)) -} diff --git a/pkg/stat/metric/iterator.go b/pkg/stat/metric/iterator.go deleted file mode 100644 index b0cac693d..000000000 --- a/pkg/stat/metric/iterator.go +++ /dev/null @@ -1,26 +0,0 @@ -package metric - -import "fmt" - -// Iterator iterates the buckets within the window. -type Iterator struct { - count int - iteratedCount int - cur *Bucket -} - -// Next returns true util all of the buckets has been iterated. -func (i *Iterator) Next() bool { - return i.count != i.iteratedCount -} - -// Bucket gets current bucket. -func (i *Iterator) Bucket() Bucket { - if !(i.Next()) { - panic(fmt.Errorf("stat/metric: iteration out of range iteratedCount: %d count: %d", i.iteratedCount, i.count)) - } - bucket := *i.cur - i.iteratedCount++ - i.cur = i.cur.Next() - return bucket -} diff --git a/pkg/stat/metric/metric.go b/pkg/stat/metric/metric.go deleted file mode 100644 index 9ea08ca72..000000000 --- a/pkg/stat/metric/metric.go +++ /dev/null @@ -1,104 +0,0 @@ -package metric - -import ( - "errors" - "fmt" -) - -// Opts contains the common arguments for creating Metric. -type Opts struct { -} - -// Metric is a sample interface. -// Implementations of Metrics in metric package are Counter, Gauge, -// PointGauge, RollingCounter and RollingGauge. -type Metric interface { - // Add adds the given value to the counter. - Add(int64) - // Value gets the current value. - // If the metric's type is PointGauge, RollingCounter, RollingGauge, - // it returns the sum value within the window. - Value() int64 -} - -// Aggregation contains some common aggregation function. -// Each aggregation can compute summary statistics of window. -type Aggregation interface { - // Min finds the min value within the window. - Min() float64 - // Max finds the max value within the window. - Max() float64 - // Avg computes average value within the window. - Avg() float64 - // Sum computes sum value within the window. - Sum() float64 -} - -// VectorOpts contains the common arguments for creating vec Metric.. -type VectorOpts struct { - Namespace string - Subsystem string - Name string - Help string - Labels []string -} - -const ( - _businessNamespace = "business" - _businessSubsystemCount = "count" - _businessSubSystemGauge = "gauge" - _businessSubSystemHistogram = "histogram" -) - -var ( - _defaultBuckets = []float64{5, 10, 25, 50, 100, 250, 500} -) - -// NewBusinessMetricCount business Metric count vec. -// name or labels should not be empty. -func NewBusinessMetricCount(name string, labels ...string) CounterVec { - if name == "" || len(labels) == 0 { - panic(errors.New("stat:metric business count metric name should not be empty or labels length should be greater than zero")) - } - return NewCounterVec(&CounterVecOpts{ - Namespace: _businessNamespace, - Subsystem: _businessSubsystemCount, - Name: name, - Labels: labels, - Help: fmt.Sprintf("business metric count %s", name), - }) -} - -// NewBusinessMetricGauge business Metric gauge vec. -// name or labels should not be empty. -func NewBusinessMetricGauge(name string, labels ...string) GaugeVec { - if name == "" || len(labels) == 0 { - panic(errors.New("stat:metric business gauge metric name should not be empty or labels length should be greater than zero")) - } - return NewGaugeVec(&GaugeVecOpts{ - Namespace: _businessNamespace, - Subsystem: _businessSubSystemGauge, - Name: name, - Labels: labels, - Help: fmt.Sprintf("business metric gauge %s", name), - }) -} - -// NewBusinessMetricHistogram business Metric histogram vec. -// name or labels should not be empty. -func NewBusinessMetricHistogram(name string, buckets []float64, labels ...string) HistogramVec { - if name == "" || len(labels) == 0 { - panic(errors.New("stat:metric business histogram metric name should not be empty or labels length should be greater than zero")) - } - if len(buckets) == 0 { - buckets = _defaultBuckets - } - return NewHistogramVec(&HistogramVecOpts{ - Namespace: _businessNamespace, - Subsystem: _businessSubSystemHistogram, - Name: name, - Labels: labels, - Buckets: buckets, - Help: fmt.Sprintf("business metric histogram %s", name), - }) -} diff --git a/pkg/stat/metric/point_gauge.go b/pkg/stat/metric/point_gauge.go deleted file mode 100644 index 0fc15a622..000000000 --- a/pkg/stat/metric/point_gauge.go +++ /dev/null @@ -1,61 +0,0 @@ -package metric - -var _ Metric = &pointGauge{} -var _ Aggregation = &pointGauge{} - -// PointGauge represents a ring window. -// Every buckets within the window contains one point. -// When the window is full, the earliest point will be overwrite. -type PointGauge interface { - Aggregation - Metric - // Reduce applies the reduction function to all buckets within the window. - Reduce(func(Iterator) float64) float64 -} - -// PointGaugeOpts contains the arguments for creating PointGauge. -type PointGaugeOpts struct { - // Size represents the bucket size within the window. - Size int -} - -type pointGauge struct { - policy *PointPolicy -} - -// NewPointGauge creates a new PointGauge based on PointGaugeOpts. -func NewPointGauge(opts PointGaugeOpts) PointGauge { - window := NewWindow(WindowOpts{Size: opts.Size}) - policy := NewPointPolicy(window) - return &pointGauge{ - policy: policy, - } -} - -func (r *pointGauge) Add(val int64) { - r.policy.Append(float64(val)) -} - -func (r *pointGauge) Reduce(f func(Iterator) float64) float64 { - return r.policy.Reduce(f) -} - -func (r *pointGauge) Avg() float64 { - return r.policy.Reduce(Avg) -} - -func (r *pointGauge) Min() float64 { - return r.policy.Reduce(Min) -} - -func (r *pointGauge) Max() float64 { - return r.policy.Reduce(Max) -} - -func (r *pointGauge) Sum() float64 { - return r.policy.Reduce(Sum) -} - -func (r *pointGauge) Value() int64 { - return int64(r.Sum()) -} diff --git a/pkg/stat/metric/point_gauge_test.go b/pkg/stat/metric/point_gauge_test.go deleted file mode 100644 index fcfee4788..000000000 --- a/pkg/stat/metric/point_gauge_test.go +++ /dev/null @@ -1,55 +0,0 @@ -package metric - -import ( - "testing" - - "github.com/stretchr/testify/assert" -) - -func TestPointGaugeAdd(t *testing.T) { - opts := PointGaugeOpts{Size: 3} - pointGauge := NewPointGauge(opts) - listBuckets := func() [][]float64 { - buckets := make([][]float64, 0) - pointGauge.Reduce(func(i Iterator) float64 { - for i.Next() { - bucket := i.Bucket() - buckets = append(buckets, bucket.Points) - } - return 0.0 - }) - return buckets - } - assert.Equal(t, [][]float64{{}, {}, {}}, listBuckets(), "Empty Buckets") - pointGauge.Add(1) - assert.Equal(t, [][]float64{{}, {}, {1}}, listBuckets(), "Point 1") - pointGauge.Add(2) - assert.Equal(t, [][]float64{{}, {1}, {2}}, listBuckets(), "Point 1, 2") - pointGauge.Add(3) - assert.Equal(t, [][]float64{{1}, {2}, {3}}, listBuckets(), "Point 1, 2, 3") - pointGauge.Add(4) - assert.Equal(t, [][]float64{{2}, {3}, {4}}, listBuckets(), "Point 2, 3, 4") - pointGauge.Add(5) - assert.Equal(t, [][]float64{{3}, {4}, {5}}, listBuckets(), "Point 3, 4, 5") -} - -func TestPointGaugeReduce(t *testing.T) { - opts := PointGaugeOpts{Size: 10} - pointGauge := NewPointGauge(opts) - for i := 0; i < opts.Size; i++ { - pointGauge.Add(int64(i)) - } - var _ = pointGauge.Reduce(func(i Iterator) float64 { - idx := 0 - for i.Next() { - bucket := i.Bucket() - assert.Equal(t, bucket.Points[0], float64(idx), "validate points of pointGauge") - idx++ - } - return 0.0 - }) - assert.Equal(t, float64(9), pointGauge.Max(), "validate max of pointGauge") - assert.Equal(t, float64(4.5), pointGauge.Avg(), "validate avg of pointGauge") - assert.Equal(t, float64(0), pointGauge.Min(), "validate min of pointGauge") - assert.Equal(t, float64(45), pointGauge.Sum(), "validate sum of pointGauge") -} diff --git a/pkg/stat/metric/point_policy.go b/pkg/stat/metric/point_policy.go deleted file mode 100644 index aae8934e9..000000000 --- a/pkg/stat/metric/point_policy.go +++ /dev/null @@ -1,57 +0,0 @@ -package metric - -import "sync" - -// PointPolicy is a policy of points within the window. -// PointPolicy wraps the window and make it seem like ring-buf. -// When using PointPolicy, every buckets within the windows contains at more one point. -// e.g. [[1], [2], [3]] -type PointPolicy struct { - mu sync.RWMutex - size int - window *Window - offset int -} - -// NewPointPolicy creates a new PointPolicy. -func NewPointPolicy(window *Window) *PointPolicy { - return &PointPolicy{ - window: window, - size: window.Size(), - offset: -1, - } -} - -func (p *PointPolicy) prevOffset() int { - return p.offset -} - -func (p *PointPolicy) nextOffset() int { - return (p.prevOffset() + 1) % p.size -} - -func (p *PointPolicy) updateOffset(offset int) { - p.offset = offset -} - -// Append appends the given points to the window. -func (p *PointPolicy) Append(val float64) { - p.mu.Lock() - defer p.mu.Unlock() - offset := p.nextOffset() - p.window.ResetBucket(offset) - p.window.Append(offset, val) - p.updateOffset(offset) -} - -// Reduce applies the reduction function to all buckets within the window. -func (p *PointPolicy) Reduce(f func(Iterator) float64) float64 { - p.mu.RLock() - defer p.mu.RUnlock() - offset := p.offset + 1 - if offset == p.size { - offset = 0 - } - iterator := p.window.Iterator(offset, p.size) - return f(iterator) -} diff --git a/pkg/stat/metric/reduce.go b/pkg/stat/metric/reduce.go deleted file mode 100644 index 20165c984..000000000 --- a/pkg/stat/metric/reduce.go +++ /dev/null @@ -1,77 +0,0 @@ -package metric - -// Sum the values within the window. -func Sum(iterator Iterator) float64 { - var result = 0.0 - for iterator.Next() { - bucket := iterator.Bucket() - for _, p := range bucket.Points { - result = result + p - } - } - return result -} - -// Avg the values within the window. -func Avg(iterator Iterator) float64 { - var result = 0.0 - var count = 0.0 - for iterator.Next() { - bucket := iterator.Bucket() - for _, p := range bucket.Points { - result = result + p - count = count + 1 - } - } - return result / count -} - -// Min the values within the window. -func Min(iterator Iterator) float64 { - var result = 0.0 - var started = false - for iterator.Next() { - bucket := iterator.Bucket() - for _, p := range bucket.Points { - if !started { - result = p - started = true - continue - } - if p < result { - result = p - } - } - } - return result -} - -// Max the values within the window. -func Max(iterator Iterator) float64 { - var result = 0.0 - var started = false - for iterator.Next() { - bucket := iterator.Bucket() - for _, p := range bucket.Points { - if !started { - result = p - started = true - continue - } - if p > result { - result = p - } - } - } - return result -} - -// Count sums the count value within the window. -func Count(iterator Iterator) float64 { - var result int64 - for iterator.Next() { - bucket := iterator.Bucket() - result += bucket.Count - } - return float64(result) -} diff --git a/pkg/stat/metric/reduce_test.go b/pkg/stat/metric/reduce_test.go deleted file mode 100644 index 5165dd293..000000000 --- a/pkg/stat/metric/reduce_test.go +++ /dev/null @@ -1,17 +0,0 @@ -package metric - -import ( - "testing" - - "github.com/stretchr/testify/assert" -) - -func TestCount(t *testing.T) { - opts := PointGaugeOpts{Size: 10} - pointGauge := NewPointGauge(opts) - for i := 0; i < opts.Size; i++ { - pointGauge.Add(int64(i)) - } - result := pointGauge.Reduce(Count) - assert.Equal(t, float64(10), result, "validate count of pointGauge") -} diff --git a/pkg/stat/metric/rolling_counter.go b/pkg/stat/metric/rolling_counter.go deleted file mode 100644 index e1038439b..000000000 --- a/pkg/stat/metric/rolling_counter.go +++ /dev/null @@ -1,75 +0,0 @@ -package metric - -import ( - "fmt" - "time" -) - -var _ Metric = &rollingCounter{} -var _ Aggregation = &rollingCounter{} - -// RollingCounter represents a ring window based on time duration. -// e.g. [[1], [3], [5]] -type RollingCounter interface { - Metric - Aggregation - Timespan() int - // Reduce applies the reduction function to all buckets within the window. - Reduce(func(Iterator) float64) float64 -} - -// RollingCounterOpts contains the arguments for creating RollingCounter. -type RollingCounterOpts struct { - Size int - BucketDuration time.Duration -} - -type rollingCounter struct { - policy *RollingPolicy -} - -// NewRollingCounter creates a new RollingCounter bases on RollingCounterOpts. -func NewRollingCounter(opts RollingCounterOpts) RollingCounter { - window := NewWindow(WindowOpts{Size: opts.Size}) - policy := NewRollingPolicy(window, RollingPolicyOpts{BucketDuration: opts.BucketDuration}) - return &rollingCounter{ - policy: policy, - } -} - -func (r *rollingCounter) Add(val int64) { - if val < 0 { - panic(fmt.Errorf("stat/metric: cannot decrease in value. val: %d", val)) - } - r.policy.Add(float64(val)) -} - -func (r *rollingCounter) Reduce(f func(Iterator) float64) float64 { - return r.policy.Reduce(f) -} - -func (r *rollingCounter) Avg() float64 { - return r.policy.Reduce(Avg) -} - -func (r *rollingCounter) Min() float64 { - return r.policy.Reduce(Min) -} - -func (r *rollingCounter) Max() float64 { - return r.policy.Reduce(Max) -} - -func (r *rollingCounter) Sum() float64 { - return r.policy.Reduce(Sum) -} - -func (r *rollingCounter) Value() int64 { - return int64(r.Sum()) -} - -func (r *rollingCounter) Timespan() int { - r.policy.mu.RLock() - defer r.policy.mu.RUnlock() - return r.policy.timespan() -} diff --git a/pkg/stat/metric/rolling_counter_test.go b/pkg/stat/metric/rolling_counter_test.go deleted file mode 100644 index 82caa52e1..000000000 --- a/pkg/stat/metric/rolling_counter_test.go +++ /dev/null @@ -1,156 +0,0 @@ -package metric - -import ( - "testing" - "time" - - "github.com/stretchr/testify/assert" -) - -func TestRollingCounterAdd(t *testing.T) { - size := 3 - bucketDuration := time.Second - opts := RollingCounterOpts{ - Size: size, - BucketDuration: bucketDuration, - } - r := NewRollingCounter(opts) - listBuckets := func() [][]float64 { - buckets := make([][]float64, 0) - r.Reduce(func(i Iterator) float64 { - for i.Next() { - bucket := i.Bucket() - buckets = append(buckets, bucket.Points) - } - return 0.0 - }) - return buckets - } - assert.Equal(t, [][]float64{{}, {}, {}}, listBuckets()) - r.Add(1) - assert.Equal(t, [][]float64{{}, {}, {1}}, listBuckets()) - time.Sleep(time.Second) - r.Add(2) - r.Add(3) - assert.Equal(t, [][]float64{{}, {1}, {5}}, listBuckets()) - time.Sleep(time.Second) - r.Add(4) - r.Add(5) - r.Add(6) - assert.Equal(t, [][]float64{{1}, {5}, {15}}, listBuckets()) - time.Sleep(time.Second) - r.Add(7) - assert.Equal(t, [][]float64{{5}, {15}, {7}}, listBuckets()) -} - -func TestRollingCounterReduce(t *testing.T) { - size := 3 - bucketDuration := time.Second - opts := RollingCounterOpts{ - Size: size, - BucketDuration: bucketDuration, - } - r := NewRollingCounter(opts) - for x := 0; x < size; x = x + 1 { - for i := 0; i <= x; i++ { - r.Add(1) - } - if x < size-1 { - time.Sleep(bucketDuration) - } - } - var result = r.Reduce(func(iterator Iterator) float64 { - var result float64 - for iterator.Next() { - bucket := iterator.Bucket() - result += bucket.Points[0] - } - return result - }) - if result != 6.0 { - t.Fatalf("Validate sum of points. result: %f", result) - } -} - -func TestRollingCounterDataRace(t *testing.T) { - size := 3 - bucketDuration := time.Millisecond * 10 - opts := RollingCounterOpts{ - Size: size, - BucketDuration: bucketDuration, - } - r := NewRollingCounter(opts) - var stop = make(chan bool) - go func() { - for { - select { - case <-stop: - return - default: - r.Add(1) - time.Sleep(time.Millisecond * 5) - } - } - }() - go func() { - for { - select { - case <-stop: - return - default: - _ = r.Reduce(func(i Iterator) float64 { - for i.Next() { - bucket := i.Bucket() - for range bucket.Points { - continue - } - } - return 0 - }) - } - } - }() - time.Sleep(time.Second * 3) - close(stop) -} - -func BenchmarkRollingCounterIncr(b *testing.B) { - size := 3 - bucketDuration := time.Millisecond * 100 - opts := RollingCounterOpts{ - Size: size, - BucketDuration: bucketDuration, - } - r := NewRollingCounter(opts) - b.ResetTimer() - for i := 0; i <= b.N; i++ { - r.Add(1) - } -} - -func BenchmarkRollingCounterReduce(b *testing.B) { - size := 3 - bucketDuration := time.Second - opts := RollingCounterOpts{ - Size: size, - BucketDuration: bucketDuration, - } - r := NewRollingCounter(opts) - for i := 0; i <= 10; i++ { - r.Add(1) - time.Sleep(time.Millisecond * 500) - } - b.ResetTimer() - for i := 0; i <= b.N; i++ { - var _ = r.Reduce(func(i Iterator) float64 { - var result float64 - for i.Next() { - bucket := i.Bucket() - if len(bucket.Points) != 0 { - result += bucket.Points[0] - } - } - return result - }) - } -} diff --git a/pkg/stat/metric/rolling_gauge.go b/pkg/stat/metric/rolling_gauge.go deleted file mode 100644 index c065a0228..000000000 --- a/pkg/stat/metric/rolling_gauge.go +++ /dev/null @@ -1,62 +0,0 @@ -package metric - -import "time" - -var _ Metric = &rollingGauge{} -var _ Aggregation = &rollingGauge{} - -// RollingGauge represents a ring window based on time duration. -// e.g. [[1, 2], [1, 2, 3], [1,2, 3, 4]] -type RollingGauge interface { - Metric - Aggregation - // Reduce applies the reduction function to all buckets within the window. - Reduce(func(Iterator) float64) float64 -} - -// RollingGaugeOpts contains the arguments for creating RollingGauge. -type RollingGaugeOpts struct { - Size int - BucketDuration time.Duration -} - -type rollingGauge struct { - policy *RollingPolicy -} - -// NewRollingGauge creates a new RollingGauge baseed on RollingGaugeOpts. -func NewRollingGauge(opts RollingGaugeOpts) RollingGauge { - window := NewWindow(WindowOpts{Size: opts.Size}) - policy := NewRollingPolicy(window, RollingPolicyOpts{BucketDuration: opts.BucketDuration}) - return &rollingGauge{ - policy: policy, - } -} - -func (r *rollingGauge) Add(val int64) { - r.policy.Append(float64(val)) -} - -func (r *rollingGauge) Reduce(f func(Iterator) float64) float64 { - return r.policy.Reduce(f) -} - -func (r *rollingGauge) Avg() float64 { - return r.policy.Reduce(Avg) -} - -func (r *rollingGauge) Min() float64 { - return r.policy.Reduce(Min) -} - -func (r *rollingGauge) Max() float64 { - return r.policy.Reduce(Max) -} - -func (r *rollingGauge) Sum() float64 { - return r.policy.Reduce(Sum) -} - -func (r *rollingGauge) Value() int64 { - return int64(r.Sum()) -} diff --git a/pkg/stat/metric/rolling_gauge_test.go b/pkg/stat/metric/rolling_gauge_test.go deleted file mode 100644 index 825ea1551..000000000 --- a/pkg/stat/metric/rolling_gauge_test.go +++ /dev/null @@ -1,192 +0,0 @@ -package metric - -import ( - "math/rand" - "testing" - "time" - - "github.com/stretchr/testify/assert" -) - -func TestRollingGaugeAdd(t *testing.T) { - size := 3 - bucketDuration := time.Second - opts := RollingGaugeOpts{ - Size: size, - BucketDuration: bucketDuration, - } - r := NewRollingGauge(opts) - listBuckets := func() [][]float64 { - buckets := make([][]float64, 0) - r.Reduce(func(i Iterator) float64 { - for i.Next() { - bucket := i.Bucket() - buckets = append(buckets, bucket.Points) - } - return 0.0 - }) - return buckets - } - assert.Equal(t, [][]float64{{}, {}, {}}, listBuckets()) - r.Add(1) - assert.Equal(t, [][]float64{{}, {}, {1}}, listBuckets()) - time.Sleep(time.Second) - r.Add(2) - r.Add(3) - assert.Equal(t, [][]float64{{}, {1}, {2, 3}}, listBuckets()) - time.Sleep(time.Second) - r.Add(4) - r.Add(5) - r.Add(6) - assert.Equal(t, [][]float64{{1}, {2, 3}, {4, 5, 6}}, listBuckets()) - time.Sleep(time.Second) - r.Add(7) - assert.Equal(t, [][]float64{{2, 3}, {4, 5, 6}, {7}}, listBuckets()) -} - -func TestRollingGaugeReset(t *testing.T) { - size := 3 - bucketDuration := time.Second - opts := RollingGaugeOpts{ - Size: size, - BucketDuration: bucketDuration, - } - r := NewRollingGauge(opts) - listBuckets := func() [][]float64 { - buckets := make([][]float64, 0) - r.Reduce(func(i Iterator) float64 { - for i.Next() { - bucket := i.Bucket() - buckets = append(buckets, bucket.Points) - } - return 0.0 - }) - return buckets - } - r.Add(1) - time.Sleep(time.Second) - assert.Equal(t, [][]float64{{}, {1}}, listBuckets()) - time.Sleep(time.Second) - assert.Equal(t, [][]float64{{1}}, listBuckets()) - time.Sleep(time.Second) - assert.Equal(t, [][]float64{}, listBuckets()) - - // cross window - r.Add(1) - time.Sleep(time.Second * 5) - assert.Equal(t, [][]float64{}, listBuckets()) -} - -func TestRollingGaugeReduce(t *testing.T) { - size := 3 - bucketDuration := time.Second - opts := RollingGaugeOpts{ - Size: size, - BucketDuration: bucketDuration, - } - r := NewRollingGauge(opts) - for x := 0; x < size; x = x + 1 { - for i := 0; i <= x; i++ { - r.Add(int64(i)) - } - if x < size-1 { - time.Sleep(bucketDuration) - } - } - var result = r.Reduce(func(i Iterator) float64 { - var result float64 - for i.Next() { - bucket := i.Bucket() - for _, point := range bucket.Points { - result += point - } - } - return result - }) - if result != 4.0 { - t.Fatalf("Validate sum of points. result: %f", result) - } -} - -func TestRollingGaugeDataRace(t *testing.T) { - size := 3 - bucketDuration := time.Second - opts := RollingGaugeOpts{ - Size: size, - BucketDuration: bucketDuration, - } - r := NewRollingGauge(opts) - var stop = make(chan bool) - go func() { - for { - select { - case <-stop: - return - default: - r.Add(rand.Int63()) - time.Sleep(time.Millisecond * 5) - } - } - }() - go func() { - for { - select { - case <-stop: - return - default: - _ = r.Reduce(func(i Iterator) float64 { - for i.Next() { - bucket := i.Bucket() - for range bucket.Points { - continue - } - } - return 0 - }) - } - } - }() - time.Sleep(time.Second * 3) - close(stop) -} - -func BenchmarkRollingGaugeIncr(b *testing.B) { - size := 10 - bucketDuration := time.Second - opts := RollingGaugeOpts{ - Size: size, - BucketDuration: bucketDuration, - } - r := NewRollingGauge(opts) - b.ResetTimer() - for i := 0; i <= b.N; i++ { - r.Add(1.0) - } -} - -func BenchmarkRollingGaugeReduce(b *testing.B) { - size := 10 - bucketDuration := time.Second - opts := RollingGaugeOpts{ - Size: size, - BucketDuration: bucketDuration, - } - r := NewRollingGauge(opts) - for i := 0; i <= 10; i++ { - r.Add(1.0) - time.Sleep(time.Millisecond * 500) - } - b.ResetTimer() - for i := 0; i <= b.N; i++ { - var _ = r.Reduce(func(i Iterator) float64 { - var result float64 - for i.Next() { - bucket := i.Bucket() - if len(bucket.Points) != 0 { - result += bucket.Points[0] - } - } - return result - }) - } -} diff --git a/pkg/stat/metric/rolling_policy.go b/pkg/stat/metric/rolling_policy.go deleted file mode 100644 index 13e5516b2..000000000 --- a/pkg/stat/metric/rolling_policy.go +++ /dev/null @@ -1,100 +0,0 @@ -package metric - -import ( - "sync" - "time" -) - -// RollingPolicy is a policy for ring window based on time duration. -// RollingPolicy moves bucket offset with time duration. -// e.g. If the last point is appended one bucket duration ago, -// RollingPolicy will increment current offset. -type RollingPolicy struct { - mu sync.RWMutex - size int - window *Window - offset int - - bucketDuration time.Duration - lastAppendTime time.Time -} - -// RollingPolicyOpts contains the arguments for creating RollingPolicy. -type RollingPolicyOpts struct { - BucketDuration time.Duration -} - -// NewRollingPolicy creates a new RollingPolicy based on the given window and RollingPolicyOpts. -func NewRollingPolicy(window *Window, opts RollingPolicyOpts) *RollingPolicy { - return &RollingPolicy{ - window: window, - size: window.Size(), - offset: 0, - - bucketDuration: opts.BucketDuration, - lastAppendTime: time.Now(), - } -} - -func (r *RollingPolicy) timespan() int { - v := int(time.Since(r.lastAppendTime) / r.bucketDuration) - if v > -1 { // maybe time backwards - return v - } - return r.size -} - -func (r *RollingPolicy) add(f func(offset int, val float64), val float64) { - r.mu.Lock() - timespan := r.timespan() - if timespan > 0 { - r.lastAppendTime = r.lastAppendTime.Add(time.Duration(timespan * int(r.bucketDuration))) - offset := r.offset - // reset the expired buckets - s := offset + 1 - if timespan > r.size { - timespan = r.size - } - e, e1 := s+timespan, 0 // e: reset offset must start from offset+1 - if e > r.size { - e1 = e - r.size - e = r.size - } - for i := s; i < e; i++ { - r.window.ResetBucket(i) - offset = i - } - for i := 0; i < e1; i++ { - r.window.ResetBucket(i) - offset = i - } - r.offset = offset - } - f(r.offset, val) - r.mu.Unlock() -} - -// Append appends the given points to the window. -func (r *RollingPolicy) Append(val float64) { - r.add(r.window.Append, val) -} - -// Add adds the given value to the latest point within bucket. -func (r *RollingPolicy) Add(val float64) { - r.add(r.window.Add, val) -} - -// Reduce applies the reduction function to all buckets within the window. -func (r *RollingPolicy) Reduce(f func(Iterator) float64) (val float64) { - r.mu.RLock() - timespan := r.timespan() - if count := r.size - timespan; count > 0 { - offset := r.offset + timespan + 1 - if offset >= r.size { - offset = offset - r.size - } - val = f(r.window.Iterator(offset, count)) - } - r.mu.RUnlock() - return val -} diff --git a/pkg/stat/metric/rolling_policy_test.go b/pkg/stat/metric/rolling_policy_test.go deleted file mode 100644 index 2792605f6..000000000 --- a/pkg/stat/metric/rolling_policy_test.go +++ /dev/null @@ -1,67 +0,0 @@ -package metric - -import ( - "fmt" - "math/rand" - "testing" - "time" -) - -func GetRollingPolicy() *RollingPolicy { - w := NewWindow(WindowOpts{Size: 10}) - return NewRollingPolicy(w, RollingPolicyOpts{BucketDuration: 300 * time.Millisecond}) -} - -func Handler(t *testing.T, table []map[string][]int) { - for _, hm := range table { - var totalTs, lastOffset int - offsetAndPoints := hm["offsetAndPoints"] - timeSleep := hm["timeSleep"] - policy := GetRollingPolicy() - for i, n := range timeSleep { - totalTs += n - time.Sleep(time.Duration(n) * time.Millisecond) - policy.Add(1) - offset, points := offsetAndPoints[2*i], offsetAndPoints[2*i+1] - - if int(policy.window.window[offset].Points[0]) != points { - t.Errorf("error, time since last append: %vms, last offset: %v", totalTs, lastOffset) - } - lastOffset = offset - } - } -} - -func TestRollingPolicy_Add(t *testing.T) { - rand.Seed(time.Now().Unix()) - - // test add after 400ms and 601ms relative to the policy created time - policy := GetRollingPolicy() - time.Sleep(400 * time.Millisecond) - policy.Add(1) - time.Sleep(201 * time.Millisecond) - policy.Add(1) - for _, b := range policy.window.window { - fmt.Println(b.Points) - } - if int(policy.window.window[1].Points[0]) != 1 { - t.Errorf("error, time since last append: %vms, last offset: %v", 300, 0) - } - if int(policy.window.window[2].Points[0]) != 1 { - t.Errorf("error, time since last append: %vms, last offset: %v", 301, 0) - } - - // test func timespan return real span - table := []map[string][]int{ - { - "timeSleep": []int{294, 3200}, - "offsetAndPoints": []int{0, 1, 0, 1}, - }, - { - "timeSleep": []int{305, 3200, 6400}, - "offsetAndPoints": []int{1, 1, 1, 1, 1, 1}, - }, - } - - Handler(t, table) -} diff --git a/pkg/stat/metric/window.go b/pkg/stat/metric/window.go deleted file mode 100644 index e8a0d9844..000000000 --- a/pkg/stat/metric/window.go +++ /dev/null @@ -1,107 +0,0 @@ -package metric - -// Bucket contains multiple float64 points. -type Bucket struct { - Points []float64 - Count int64 - next *Bucket -} - -// Append appends the given value to the bucket. -func (b *Bucket) Append(val float64) { - b.Points = append(b.Points, val) - b.Count++ -} - -// Add adds the given value to the point. -func (b *Bucket) Add(offset int, val float64) { - b.Points[offset] += val - b.Count++ -} - -// Reset empties the bucket. -func (b *Bucket) Reset() { - b.Points = b.Points[:0] - b.Count = 0 -} - -// Next returns the next bucket. -func (b *Bucket) Next() *Bucket { - return b.next -} - -// Window contains multiple buckets. -type Window struct { - window []Bucket - size int -} - -// WindowOpts contains the arguments for creating Window. -type WindowOpts struct { - Size int -} - -// NewWindow creates a new Window based on WindowOpts. -func NewWindow(opts WindowOpts) *Window { - buckets := make([]Bucket, opts.Size) - for offset := range buckets { - buckets[offset] = Bucket{Points: make([]float64, 0)} - nextOffset := offset + 1 - if nextOffset == opts.Size { - nextOffset = 0 - } - buckets[offset].next = &buckets[nextOffset] - } - return &Window{window: buckets, size: opts.Size} -} - -// ResetWindow empties all buckets within the window. -func (w *Window) ResetWindow() { - for offset := range w.window { - w.ResetBucket(offset) - } -} - -// ResetBucket empties the bucket based on the given offset. -func (w *Window) ResetBucket(offset int) { - w.window[offset].Reset() -} - -// ResetBuckets empties the buckets based on the given offsets. -func (w *Window) ResetBuckets(offsets []int) { - for _, offset := range offsets { - w.ResetBucket(offset) - } -} - -// Append appends the given value to the bucket where index equals the given offset. -func (w *Window) Append(offset int, val float64) { - w.window[offset].Append(val) -} - -// Add adds the given value to the latest point within bucket where index equals the given offset. -func (w *Window) Add(offset int, val float64) { - if w.window[offset].Count == 0 { - w.window[offset].Append(val) - return - } - w.window[offset].Add(0, val) -} - -// Bucket returns the bucket where index equals the given offset. -func (w *Window) Bucket(offset int) Bucket { - return w.window[offset] -} - -// Size returns the size of the window. -func (w *Window) Size() int { - return w.size -} - -// Iterator returns the bucket iterator. -func (w *Window) Iterator(offset int, count int) Iterator { - return Iterator{ - count: count, - cur: &w.window[offset], - } -} diff --git a/pkg/stat/metric/window_test.go b/pkg/stat/metric/window_test.go deleted file mode 100644 index e49699a12..000000000 --- a/pkg/stat/metric/window_test.go +++ /dev/null @@ -1,68 +0,0 @@ -package metric - -import ( - "testing" - - "github.com/stretchr/testify/assert" -) - -func TestWindowResetWindow(t *testing.T) { - opts := WindowOpts{Size: 3} - window := NewWindow(opts) - for i := 0; i < opts.Size; i++ { - window.Append(i, 1.0) - } - window.ResetWindow() - for i := 0; i < opts.Size; i++ { - assert.Equal(t, len(window.Bucket(i).Points), 0) - } -} - -func TestWindowResetBucket(t *testing.T) { - opts := WindowOpts{Size: 3} - window := NewWindow(opts) - for i := 0; i < opts.Size; i++ { - window.Append(i, 1.0) - } - window.ResetBucket(1) - assert.Equal(t, len(window.Bucket(1).Points), 0) - assert.Equal(t, window.Bucket(0).Points[0], float64(1.0)) - assert.Equal(t, window.Bucket(2).Points[0], float64(1.0)) -} - -func TestWindowResetBuckets(t *testing.T) { - opts := WindowOpts{Size: 3} - window := NewWindow(opts) - for i := 0; i < opts.Size; i++ { - window.Append(i, 1.0) - } - window.ResetBuckets([]int{0, 1, 2}) - for i := 0; i < opts.Size; i++ { - assert.Equal(t, len(window.Bucket(i).Points), 0) - } -} - -func TestWindowAppend(t *testing.T) { - opts := WindowOpts{Size: 3} - window := NewWindow(opts) - for i := 0; i < opts.Size; i++ { - window.Append(i, 1.0) - } - for i := 0; i < opts.Size; i++ { - assert.Equal(t, window.Bucket(i).Points[0], float64(1.0)) - } -} - -func TestWindowAdd(t *testing.T) { - opts := WindowOpts{Size: 3} - window := NewWindow(opts) - window.Append(0, 1.0) - window.Add(0, 1.0) - assert.Equal(t, window.Bucket(0).Points[0], float64(2.0)) -} - -func TestWindowSize(t *testing.T) { - opts := WindowOpts{Size: 3} - window := NewWindow(opts) - assert.Equal(t, window.Size(), 3) -} diff --git a/pkg/stat/sys/cpu/README.md b/pkg/stat/sys/cpu/README.md deleted file mode 100644 index 1028e9e03..000000000 --- a/pkg/stat/sys/cpu/README.md +++ /dev/null @@ -1,7 +0,0 @@ -## stat/sys - -System Information - -## 项目简介 - -获取Linux平台下的系统信息,包括cpu主频、cpu使用率等 diff --git a/pkg/stat/sys/cpu/cgroup.go b/pkg/stat/sys/cpu/cgroup.go deleted file mode 100644 index b297f611f..000000000 --- a/pkg/stat/sys/cpu/cgroup.go +++ /dev/null @@ -1,126 +0,0 @@ -package cpu - -import ( - "bufio" - "fmt" - "io" - "os" - "path" - "strconv" - "strings" -) - -const cgroupRootDir = "/sys/fs/cgroup" - -// cgroup Linux cgroup -type cgroup struct { - cgroupSet map[string]string -} - -// CPUCFSQuotaUs cpu.cfs_quota_us -func (c *cgroup) CPUCFSQuotaUs() (int64, error) { - data, err := readFile(path.Join(c.cgroupSet["cpu"], "cpu.cfs_quota_us")) - if err != nil { - return 0, err - } - return strconv.ParseInt(data, 10, 64) -} - -// CPUCFSPeriodUs cpu.cfs_period_us -func (c *cgroup) CPUCFSPeriodUs() (uint64, error) { - data, err := readFile(path.Join(c.cgroupSet["cpu"], "cpu.cfs_period_us")) - if err != nil { - return 0, err - } - return parseUint(data) -} - -// CPUAcctUsage cpuacct.usage -func (c *cgroup) CPUAcctUsage() (uint64, error) { - data, err := readFile(path.Join(c.cgroupSet["cpuacct"], "cpuacct.usage")) - if err != nil { - return 0, err - } - return parseUint(data) -} - -// CPUAcctUsagePerCPU cpuacct.usage_percpu -func (c *cgroup) CPUAcctUsagePerCPU() ([]uint64, error) { - data, err := readFile(path.Join(c.cgroupSet["cpuacct"], "cpuacct.usage_percpu")) - if err != nil { - return nil, err - } - var usage []uint64 - for _, v := range strings.Fields(string(data)) { - var u uint64 - if u, err = parseUint(v); err != nil { - return nil, err - } - // fix possible_cpu:https://www.ibm.com/support/knowledgecenter/en/linuxonibm/com.ibm.linux.z.lgdd/lgdd_r_posscpusparm.html - if u != 0 { - usage = append(usage, u) - } - } - return usage, nil -} - -// CPUSetCPUs cpuset.cpus -func (c *cgroup) CPUSetCPUs() ([]uint64, error) { - data, err := readFile(path.Join(c.cgroupSet["cpuset"], "cpuset.cpus")) - if err != nil { - return nil, err - } - cpus, err := ParseUintList(data) - if err != nil { - return nil, err - } - var sets []uint64 - for k := range cpus { - sets = append(sets, uint64(k)) - } - return sets, nil -} - -// CurrentcGroup get current process cgroup -func currentcGroup() (*cgroup, error) { - pid := os.Getpid() - cgroupFile := fmt.Sprintf("/proc/%d/cgroup", pid) - cgroupSet := make(map[string]string) - fp, err := os.Open(cgroupFile) - if err != nil { - return nil, err - } - defer fp.Close() - buf := bufio.NewReader(fp) - for { - line, err := buf.ReadString('\n') - if err != nil { - if err == io.EOF { - break - } - return nil, err - } - col := strings.Split(strings.TrimSpace(line), ":") - if len(col) != 3 { - return nil, fmt.Errorf("invalid cgroup format %s", line) - } - dir := col[2] - // When dir is not equal to /, it must be in docker - if dir != "/" { - cgroupSet[col[1]] = path.Join(cgroupRootDir, col[1]) - if strings.Contains(col[1], ",") { - for _, k := range strings.Split(col[1], ",") { - cgroupSet[k] = path.Join(cgroupRootDir, k) - } - } - } else { - cgroupSet[col[1]] = path.Join(cgroupRootDir, col[1], col[2]) - if strings.Contains(col[1], ",") { - for _, k := range strings.Split(col[1], ",") { - cgroupSet[k] = path.Join(cgroupRootDir, k, col[2]) - } - } - } - } - return &cgroup{cgroupSet: cgroupSet}, nil -} diff --git a/pkg/stat/sys/cpu/cgroupCPU.go b/pkg/stat/sys/cpu/cgroupCPU.go deleted file mode 100644 index db1372ae9..000000000 --- a/pkg/stat/sys/cpu/cgroupCPU.go +++ /dev/null @@ -1,250 +0,0 @@ -package cpu - -import ( - "bufio" - "fmt" - "os" - "strconv" - "strings" - - "github.com/pkg/errors" - pscpu "github.com/shirou/gopsutil/cpu" -) - -type cgroupCPU struct { - frequency uint64 - quota float64 - cores uint64 - - preSystem uint64 - preTotal uint64 - usage uint64 -} - -func newCgroupCPU() (cpu *cgroupCPU, err error) { - var cores int - cores, err = pscpu.Counts(true) - if err != nil || cores == 0 { - var cpus []uint64 - cpus, err = perCPUUsage() - if err != nil { - err = errors.Errorf("perCPUUsage() failed!err:=%v", err) - return - } - cores = len(cpus) - } - - sets, err := cpuSets() - if err != nil { - err = errors.Errorf("cpuSets() failed!err:=%v", err) - return - } - quota := float64(len(sets)) - cq, err := cpuQuota() - if err == nil && cq != -1 { - var period uint64 - if period, err = cpuPeriod(); err != nil { - err = errors.Errorf("cpuPeriod() failed!err:=%v", err) - return - } - limit := float64(cq) / float64(period) - if limit < quota { - quota = limit - } - } - maxFreq := cpuMaxFreq() - - preSystem, err := systemCPUUsage() - if err != nil { - err = errors.Errorf("systemCPUUsage() failed!err:=%v", err) - return - } - preTotal, err := totalCPUUsage() - if err != nil { - err = errors.Errorf("totalCPUUsage() failed!err:=%v", err) - return - } - cpu = &cgroupCPU{ - frequency: maxFreq, - quota: quota, - cores: uint64(cores), - preSystem: preSystem, - preTotal: preTotal, - } - return -} - -func (cpu *cgroupCPU) Usage() (u uint64, err error) { - var ( - total uint64 - system uint64 - ) - total, err = totalCPUUsage() - if err != nil { - return - } - system, err = systemCPUUsage() - if err != nil { - return - } - if system != cpu.preSystem { - u = uint64(float64((total-cpu.preTotal)*cpu.cores*1e3) / (float64(system-cpu.preSystem) * cpu.quota)) - } - cpu.preSystem = system - cpu.preTotal = total - return -} - -func (cpu *cgroupCPU) Info() Info { - return Info{ - Frequency: cpu.frequency, - Quota: cpu.quota, - } -} - -const nanoSecondsPerSecond = 1e9 - -// ErrNoCFSLimit is no quota limit -var ErrNoCFSLimit = errors.Errorf("no quota limit") - -var clockTicksPerSecond = uint64(getClockTicks()) - -// systemCPUUsage returns the host system's cpu usage in -// nanoseconds. An error is returned if the format of the underlying -// file does not match. -// -// Uses /proc/stat defined by POSIX. Looks for the cpu -// statistics line and then sums up the first seven fields -// provided. See man 5 proc for details on specific field -// information. -func systemCPUUsage() (usage uint64, err error) { - var ( - line string - f *os.File - ) - if f, err = os.Open("/proc/stat"); err != nil { - return - } - bufReader := bufio.NewReaderSize(nil, 128) - defer func() { - bufReader.Reset(nil) - f.Close() - }() - bufReader.Reset(f) - for err == nil { - if line, err = bufReader.ReadString('\n'); err != nil { - err = errors.WithStack(err) - return - } - parts := strings.Fields(line) - switch parts[0] { - case "cpu": - if len(parts) < 8 { - err = errors.WithStack(fmt.Errorf("bad format of cpu stats")) - return - } - var totalClockTicks uint64 - for _, i := range parts[1:8] { - var v uint64 - if v, err = strconv.ParseUint(i, 10, 64); err != nil { - err = errors.WithStack(fmt.Errorf("error parsing cpu stats")) - return - } - totalClockTicks += v - } - usage = (totalClockTicks * nanoSecondsPerSecond) / clockTicksPerSecond - return - } - } - err = errors.Errorf("bad stats format") - return -} - -func totalCPUUsage() (usage uint64, err error) { - var cg *cgroup - if cg, err = currentcGroup(); err != nil { - return - } - return cg.CPUAcctUsage() -} - -func perCPUUsage() (usage []uint64, err error) { - var cg *cgroup - if cg, err = currentcGroup(); err != nil { - return - } - return cg.CPUAcctUsagePerCPU() -} - -func cpuSets() (sets []uint64, err error) { - var cg *cgroup - if cg, err = currentcGroup(); err != nil { - return - } - return cg.CPUSetCPUs() -} - -func cpuQuota() (quota int64, err error) { - var cg *cgroup - if cg, err = currentcGroup(); err != nil { - return - } - return cg.CPUCFSQuotaUs() -} - -func cpuPeriod() (peroid uint64, err error) { - var cg *cgroup - if cg, err = currentcGroup(); err != nil { - return - } - return cg.CPUCFSPeriodUs() -} - -func cpuFreq() uint64 { - lines, err := readLines("/proc/cpuinfo") - if err != nil { - return 0 - } - for _, line := range lines { - fields := strings.Split(line, ":") - if len(fields) < 2 { - continue - } - key := strings.TrimSpace(fields[0]) - value := strings.TrimSpace(fields[1]) - if key == "cpu MHz" || key == "clock" { - // treat this as the fallback value, thus we ignore error - if t, err := strconv.ParseFloat(strings.Replace(value, "MHz", "", 1), 64); err == nil { - return uint64(t * 1000.0 * 1000.0) - } - } - } - return 0 -} - -func cpuMaxFreq() uint64 { - feq := cpuFreq() - data, err := readFile("/sys/devices/system/cpu/cpu0/cpufreq/cpuinfo_max_freq") - if err != nil { - return feq - } - // override the max freq from /proc/cpuinfo - cfeq, err := parseUint(data) - if err == nil { - feq = cfeq - } - return feq -} - -//GetClockTicks get the OS's ticks per second -func getClockTicks() int { - // TODO figure out a better alternative for platforms where we're missing cgo - // - // TODO Windows. This could be implemented using Win32 QueryPerformanceFrequency(). - // https://msdn.microsoft.com/en-us/library/windows/desktop/ms644905(v=vs.85).aspx - // - // An example of its usage can be found here. - // https://msdn.microsoft.com/en-us/library/windows/desktop/dn553408(v=vs.85).aspx - - return 100 -} diff --git a/pkg/stat/sys/cpu/cgroup_test.go b/pkg/stat/sys/cpu/cgroup_test.go deleted file mode 100644 index 9fbb1d151..000000000 --- a/pkg/stat/sys/cpu/cgroup_test.go +++ /dev/null @@ -1,11 +0,0 @@ -// +build linux - -package cpu - -import ( - "testing" -) - -func TestCGroup(t *testing.T) { - // TODO -} diff --git a/pkg/stat/sys/cpu/cpu.go b/pkg/stat/sys/cpu/cpu.go deleted file mode 100644 index 92d8a5c78..000000000 --- a/pkg/stat/sys/cpu/cpu.go +++ /dev/null @@ -1,68 +0,0 @@ -package cpu - -import ( - "fmt" - "sync/atomic" - "time" -) - -const ( - interval time.Duration = time.Millisecond * 500 -) - -var ( - stats CPU - usage uint64 -) - -// CPU is cpu stat usage. -type CPU interface { - Usage() (u uint64, e error) - Info() Info -} - -func init() { - var ( - err error - ) - stats, err = newCgroupCPU() - if err != nil { - // fmt.Printf("cgroup cpu init failed(%v),switch to psutil cpu\n", err) - stats, err = newPsutilCPU(interval) - if err != nil { - panic(fmt.Sprintf("cgroup cpu init failed!err:=%v", err)) - } - } - go func() { - ticker := time.NewTicker(interval) - defer ticker.Stop() - for { - <-ticker.C - u, err := stats.Usage() - if err == nil && u != 0 { - atomic.StoreUint64(&usage, u) - } - } - }() -} - -// Stat cpu stat. -type Stat struct { - Usage uint64 // cpu use ratio. -} - -// Info cpu info. -type Info struct { - Frequency uint64 - Quota float64 -} - -// ReadStat read cpu stat. -func ReadStat(stat *Stat) { - stat.Usage = atomic.LoadUint64(&usage) -} - -// GetInfo get cpu info. -func GetInfo() Info { - return stats.Info() -} diff --git a/pkg/stat/sys/cpu/cpu_test.go b/pkg/stat/sys/cpu/cpu_test.go deleted file mode 100644 index 336abe8ff..000000000 --- a/pkg/stat/sys/cpu/cpu_test.go +++ /dev/null @@ -1,22 +0,0 @@ -package cpu - -import ( - "fmt" - "testing" - "time" -) - -func Test_CPUUsage(t *testing.T) { - var stat Stat - ReadStat(&stat) - fmt.Println(stat) - time.Sleep(time.Millisecond * 1000) - for i := 0; i < 6; i++ { - time.Sleep(time.Millisecond * 500) - ReadStat(&stat) - if stat.Usage == 0 { - t.Fatalf("get cpu failed!cpu usage is zero!") - } - fmt.Println(stat) - } -} diff --git a/pkg/stat/sys/cpu/psutilCPU.go b/pkg/stat/sys/cpu/psutilCPU.go deleted file mode 100644 index 8d64f8dcc..000000000 --- a/pkg/stat/sys/cpu/psutilCPU.go +++ /dev/null @@ -1,45 +0,0 @@ -package cpu - -import ( - "time" - - "github.com/shirou/gopsutil/cpu" -) - -type psutilCPU struct { - interval time.Duration -} - -func newPsutilCPU(interval time.Duration) (cpu *psutilCPU, err error) { - cpu = &psutilCPU{interval: interval} - _, err = cpu.Usage() - if err != nil { - return - } - return -} - -func (ps *psutilCPU) Usage() (u uint64, err error) { - var percents []float64 - percents, err = cpu.Percent(ps.interval, false) - if err == nil { - u = uint64(percents[0] * 10) - } - return -} - -func (ps *psutilCPU) Info() (info Info) { - stats, err := cpu.Info() - if err != nil { - return - } - cores, err := cpu.Counts(true) - if err != nil { - return - } - info = Info{ - Frequency: uint64(stats[0].Mhz), - Quota: float64(cores), - } - return -} diff --git a/pkg/stat/sys/cpu/stat_test.go b/pkg/stat/sys/cpu/stat_test.go deleted file mode 100644 index ed2783043..000000000 --- a/pkg/stat/sys/cpu/stat_test.go +++ /dev/null @@ -1,20 +0,0 @@ -package cpu - -import ( - "testing" - "time" - - "github.com/stretchr/testify/assert" -) - -func TestStat(t *testing.T) { - time.Sleep(time.Second * 2) - var s Stat - var i Info - ReadStat(&s) - i = GetInfo() - - assert.NotZero(t, s.Usage) - assert.NotZero(t, i.Frequency) - assert.NotZero(t, i.Quota) -} diff --git a/pkg/stat/sys/cpu/util.go b/pkg/stat/sys/cpu/util.go deleted file mode 100644 index 25df1f9d1..000000000 --- a/pkg/stat/sys/cpu/util.go +++ /dev/null @@ -1,121 +0,0 @@ -package cpu - -import ( - "bufio" - "io/ioutil" - "os" - "strconv" - "strings" - - "github.com/pkg/errors" -) - -func readFile(path string) (string, error) { - contents, err := ioutil.ReadFile(path) - if err != nil { - return "", errors.Wrapf(err, "os/stat: read file(%s) failed!", path) - } - return strings.TrimSpace(string(contents)), nil -} - -func parseUint(s string) (uint64, error) { - v, err := strconv.ParseUint(s, 10, 64) - if err != nil { - intValue, intErr := strconv.ParseInt(s, 10, 64) - // 1. Handle negative values greater than MinInt64 (and) - // 2. Handle negative values lesser than MinInt64 - if intErr == nil && intValue < 0 { - return 0, nil - } else if intErr != nil && - intErr.(*strconv.NumError).Err == strconv.ErrRange && - intValue < 0 { - return 0, nil - } - return 0, errors.Wrapf(err, "os/stat: parseUint(%s) failed!", s) - } - return v, nil -} - -// ParseUintList parses and validates the specified string as the value -// found in some cgroup file (e.g. cpuset.cpus, cpuset.mems), which could be -// one of the formats below. Note that duplicates are actually allowed in the -// input string. It returns a map[int]bool with available elements from val -// set to true. -// Supported formats: -// 7 -// 1-6 -// 0,3-4,7,8-10 -// 0-0,0,1-7 -// 03,1-3 <- this is gonna get parsed as [1,2,3] -// 3,2,1 -// 0-2,3,1 -func ParseUintList(val string) (map[int]bool, error) { - if val == "" { - return map[int]bool{}, nil - } - - availableInts := make(map[int]bool) - split := strings.Split(val, ",") - errInvalidFormat := errors.Errorf("os/stat: invalid format: %s", val) - for _, r := range split { - if !strings.Contains(r, "-") { - v, err := strconv.Atoi(r) - if err != nil { - return nil, errInvalidFormat - } - availableInts[v] = true - } else { - split := strings.SplitN(r, "-", 2) - min, err := strconv.Atoi(split[0]) - if err != nil { - return nil, errInvalidFormat - } - max, err := strconv.Atoi(split[1]) - if err != nil { - return nil, errInvalidFormat - } - if max < min { - return nil, errInvalidFormat - } - for i := min; i <= max; i++ { - availableInts[i] = true - } - } - } - return availableInts, nil -} - -// ReadLines reads contents from a file and splits them by new lines. -// A convenience wrapper to ReadLinesOffsetN(filename, 0, -1). -func readLines(filename string) ([]string, error) { - return readLinesOffsetN(filename, 0, -1) -} - -// ReadLinesOffsetN reads contents from file and splits them by new line. -// The offset tells at which line number to start. -// The count determines the number of lines to read (starting from offset): -// n >= 0: at most n lines -// n < 0: whole file -func readLinesOffsetN(filename string, offset uint, n int) ([]string, error) { - f, err := os.Open(filename) - if err != nil { - return []string{""}, err - } - defer f.Close() - - var ret []string - - r := bufio.NewReader(f) - for i := 0; i < n+int(offset) || n < 0; i++ { - line, err := r.ReadString('\n') - if err != nil { - break - } - if i < int(offset) { - continue - } - ret = append(ret, strings.Trim(line, "\n")) - } - - return ret, nil -} diff --git a/pkg/str/str.go b/pkg/str/str.go deleted file mode 100644 index 8edc95942..000000000 --- a/pkg/str/str.go +++ /dev/null @@ -1,55 +0,0 @@ -package str - -import ( - "bytes" - "strconv" - "strings" - "sync" -) - -var ( - bfPool = sync.Pool{ - New: func() interface{} { - return bytes.NewBuffer([]byte{}) - }, - } -) - -// JoinInts format int64 slice like:n1,n2,n3. -func JoinInts(is []int64) string { - if len(is) == 0 { - return "" - } - if len(is) == 1 { - return strconv.FormatInt(is[0], 10) - } - buf := bfPool.Get().(*bytes.Buffer) - for _, i := range is { - buf.WriteString(strconv.FormatInt(i, 10)) - buf.WriteByte(',') - } - if buf.Len() > 0 { - buf.Truncate(buf.Len() - 1) - } - s := buf.String() - buf.Reset() - bfPool.Put(buf) - return s -} - -// SplitInts split string into int64 slice. -func SplitInts(s string) ([]int64, error) { - if s == "" { - return nil, nil - } - sArr := strings.Split(s, ",") - res := make([]int64, 0, len(sArr)) - for _, sc := range sArr { - i, err := strconv.ParseInt(sc, 10, 64) - if err != nil { - return nil, err - } - res = append(res, i) - } - return res, nil -} diff --git a/pkg/str/str_test.go b/pkg/str/str_test.go deleted file mode 100644 index bd009370c..000000000 --- a/pkg/str/str_test.go +++ /dev/null @@ -1,60 +0,0 @@ -package str - -import ( - "testing" -) - -func TestJoinInts(t *testing.T) { - // test empty slice - is := []int64{} - s := JoinInts(is) - if s != "" { - t.Errorf("input:%v,output:%s,result is incorrect", is, s) - } else { - t.Logf("input:%v,output:%s", is, s) - } - // test len(slice)==1 - is = []int64{1} - s = JoinInts(is) - if s != "1" { - t.Errorf("input:%v,output:%s,result is incorrect", is, s) - } else { - t.Logf("input:%v,output:%s", is, s) - } - // test len(slice)>1 - is = []int64{1, 2, 3} - s = JoinInts(is) - if s != "1,2,3" { - t.Errorf("input:%v,output:%s,result is incorrect", is, s) - } else { - t.Logf("input:%v,output:%s", is, s) - } -} - -func TestSplitInts(t *testing.T) { - // test empty slice - s := "" - is, err := SplitInts(s) - if err != nil || len(is) != 0 { - t.Error(err) - } - // test split int64 - s = "1,2,3" - is, err = SplitInts(s) - if err != nil || len(is) != 3 { - t.Error(err) - } -} - -func BenchmarkJoinInts(b *testing.B) { - is := make([]int64, 10000, 10000) - for i := int64(0); i < 10000; i++ { - is[i] = i - } - b.ResetTimer() - b.RunParallel(func(pb *testing.PB) { - for pb.Next() { - JoinInts(is) - } - }) -} diff --git a/pkg/sync/errgroup/README.md b/pkg/sync/errgroup/README.md deleted file mode 100644 index d665400da..000000000 --- a/pkg/sync/errgroup/README.md +++ /dev/null @@ -1,3 +0,0 @@ -# errgroup - -提供带recover和并行数的errgroup,err中包含详细堆栈信息 diff --git a/pkg/sync/errgroup/doc.go b/pkg/sync/errgroup/doc.go deleted file mode 100644 index b35cff432..000000000 --- a/pkg/sync/errgroup/doc.go +++ /dev/null @@ -1,47 +0,0 @@ -// Package errgroup provides synchronization, error propagation, and Context -// errgroup 包为一组子任务的 goroutine 提供了 goroutine 同步,错误取消功能. -// -//errgroup 包含三种常用方式 -// -//1、直接使用 此时不会因为一个任务失败导致所有任务被 cancel: -// g := &errgroup.Group{} -// g.Go(func(ctx context.Context) { -// // NOTE: 此时 ctx 为 context.Background() -// // do something -// }) -// -//2、WithContext 使用 WithContext 时不会因为一个任务失败导致所有任务被 cancel: -// g := errgroup.WithContext(ctx) -// g.Go(func(ctx context.Context) { -// // NOTE: 此时 ctx 为 errgroup.WithContext 传递的 ctx -// // do something -// }) -// -//3、WithCancel 使用 WithCancel 时如果有一个人任务失败会导致所有*未进行或进行中*的任务被 cancel: -// g := errgroup.WithCancel(ctx) -// g.Go(func(ctx context.Context) { -// // NOTE: 此时 ctx 是从 errgroup.WithContext 传递的 ctx 派生出的 ctx -// // do something -// }) -// -//设置最大并行数 GOMAXPROCS 对以上三种使用方式均起效 -//NOTE: 由于 errgroup 实现问题,设定 GOMAXPROCS 的 errgroup 需要立即调用 Wait() 例如: -// -// g := errgroup.WithCancel(ctx) -// g.GOMAXPROCS(2) -// // task1 -// g.Go(func(ctx context.Context) { -// fmt.Println("task1") -// }) -// // task2 -// g.Go(func(ctx context.Context) { -// fmt.Println("task2") -// }) -// // task3 -// g.Go(func(ctx context.Context) { -// fmt.Println("task3") -// }) -// // NOTE: 此时设置的 GOMAXPROCS 为2, 添加了三个任务 task1, task2, task3 此时 task3 是不会运行的! -// // 只有调用了 Wait task3 才有运行的机会 -// g.Wait() // task3 运行 -package errgroup diff --git a/pkg/sync/errgroup/errgroup.go b/pkg/sync/errgroup/errgroup.go deleted file mode 100644 index c795e1141..000000000 --- a/pkg/sync/errgroup/errgroup.go +++ /dev/null @@ -1,119 +0,0 @@ -package errgroup - -import ( - "context" - "fmt" - "runtime" - "sync" -) - -// A Group is a collection of goroutines working on subtasks that are part of -// the same overall task. -// -// A zero Group is valid and does not cancel on error. -type Group struct { - err error - wg sync.WaitGroup - errOnce sync.Once - - workerOnce sync.Once - ch chan func(ctx context.Context) error - chs []func(ctx context.Context) error - - ctx context.Context - cancel func() -} - -// WithContext create a Group. -// given function from Go will receive this context, -func WithContext(ctx context.Context) *Group { - return &Group{ctx: ctx} -} - -// WithCancel create a new Group and an associated Context derived from ctx. -// -// given function from Go will receive context derived from this ctx, -// The derived Context is canceled the first time a function passed to Go -// returns a non-nil error or the first time Wait returns, whichever occurs -// first. -func WithCancel(ctx context.Context) *Group { - ctx, cancel := context.WithCancel(ctx) - return &Group{ctx: ctx, cancel: cancel} -} - -func (g *Group) do(f func(ctx context.Context) error) { - ctx := g.ctx - if ctx == nil { - ctx = context.Background() - } - var err error - defer func() { - if r := recover(); r != nil { - buf := make([]byte, 64<<10) - buf = buf[:runtime.Stack(buf, false)] - err = fmt.Errorf("errgroup: panic recovered: %s\n%s", r, buf) - } - if err != nil { - g.errOnce.Do(func() { - g.err = err - if g.cancel != nil { - g.cancel() - } - }) - } - g.wg.Done() - }() - err = f(ctx) -} - -// GOMAXPROCS set max goroutine to work. -func (g *Group) GOMAXPROCS(n int) { - if n <= 0 { - panic("errgroup: GOMAXPROCS must great than 0") - } - g.workerOnce.Do(func() { - g.ch = make(chan func(context.Context) error, n) - for i := 0; i < n; i++ { - go func() { - for f := range g.ch { - g.do(f) - } - }() - } - }) -} - -// Go calls the given function in a new goroutine. -// -// The first call to return a non-nil error cancels the group; its error will be -// returned by Wait. -func (g *Group) Go(f func(ctx context.Context) error) { - g.wg.Add(1) - if g.ch != nil { - select { - case g.ch <- f: - default: - g.chs = append(g.chs, f) - } - return - } - go g.do(f) -} - -// Wait blocks until all function calls from the Go method have returned, then -// returns the first non-nil error (if any) from them. -func (g *Group) Wait() error { - if g.ch != nil { - for _, f := range g.chs { - g.ch <- f - } - } - g.wg.Wait() - if g.ch != nil { - close(g.ch) // let all receiver exit - } - if g.cancel != nil { - g.cancel() - } - return g.err -} diff --git a/pkg/sync/errgroup/errgroup_test.go b/pkg/sync/errgroup/errgroup_test.go deleted file mode 100644 index bb050c160..000000000 --- a/pkg/sync/errgroup/errgroup_test.go +++ /dev/null @@ -1,266 +0,0 @@ -package errgroup - -import ( - "context" - "errors" - "fmt" - "math" - "net/http" - "os" - "testing" - "time" -) - -type ABC struct { - CBA int -} - -func TestNormal(t *testing.T) { - var ( - abcs = make(map[int]*ABC) - g Group - err error - ) - for i := 0; i < 10; i++ { - abcs[i] = &ABC{CBA: i} - } - g.Go(func(context.Context) (err error) { - abcs[1].CBA++ - return - }) - g.Go(func(context.Context) (err error) { - abcs[2].CBA++ - return - }) - if err = g.Wait(); err != nil { - t.Log(err) - } - t.Log(abcs) -} - -func sleep1s(context.Context) error { - time.Sleep(time.Second) - return nil -} - -func TestGOMAXPROCS(t *testing.T) { - // 没有并发数限制 - g := Group{} - now := time.Now() - g.Go(sleep1s) - g.Go(sleep1s) - g.Go(sleep1s) - g.Go(sleep1s) - g.Wait() - sec := math.Round(time.Since(now).Seconds()) - if sec != 1 { - t.FailNow() - } - // 限制并发数 - g2 := Group{} - g2.GOMAXPROCS(2) - now = time.Now() - g2.Go(sleep1s) - g2.Go(sleep1s) - g2.Go(sleep1s) - g2.Go(sleep1s) - g2.Wait() - sec = math.Round(time.Since(now).Seconds()) - if sec != 2 { - t.FailNow() - } - // context canceled - var canceled bool - g3 := WithCancel(context.Background()) - g3.GOMAXPROCS(2) - g3.Go(func(context.Context) error { - return fmt.Errorf("error for testing errgroup context") - }) - g3.Go(func(ctx context.Context) error { - time.Sleep(time.Second) - select { - case <-ctx.Done(): - canceled = true - default: - } - return nil - }) - g3.Wait() - if !canceled { - t.FailNow() - } -} - -func TestRecover(t *testing.T) { - var ( - abcs = make(map[int]*ABC) - g Group - err error - ) - g.Go(func(context.Context) (err error) { - abcs[1].CBA++ - return - }) - g.Go(func(context.Context) (err error) { - abcs[2].CBA++ - return - }) - if err = g.Wait(); err != nil { - t.Logf("error:%+v", err) - return - } - t.FailNow() -} - -func TestRecover2(t *testing.T) { - var ( - g Group - err error - ) - g.Go(func(context.Context) (err error) { - panic("2233") - }) - if err = g.Wait(); err != nil { - t.Logf("error:%+v", err) - return - } - t.FailNow() -} - -var ( - Web = fakeSearch("web") - Image = fakeSearch("image") - Video = fakeSearch("video") -) - -type Result string -type Search func(ctx context.Context, query string) (Result, error) - -func fakeSearch(kind string) Search { - return func(_ context.Context, query string) (Result, error) { - return Result(fmt.Sprintf("%s result for %q", kind, query)), nil - } -} - -// JustErrors illustrates the use of a Group in place of a sync.WaitGroup to -// simplify goroutine counting and error handling. This example is derived from -// the sync.WaitGroup example at https://golang.org/pkg/sync/#example_WaitGroup. -func ExampleGroup_justErrors() { - var g Group - var urls = []string{ - "http://www.golang.org/", - "http://www.google.com/", - "http://www.somestupidname.com/", - } - for _, url := range urls { - // Launch a goroutine to fetch the URL. - url := url // https://golang.org/doc/faq#closures_and_goroutines - g.Go(func(context.Context) error { - // Fetch the URL. - resp, err := http.Get(url) - if err == nil { - resp.Body.Close() - } - return err - }) - } - // Wait for all HTTP fetches to complete. - if err := g.Wait(); err == nil { - fmt.Println("Successfully fetched all URLs.") - } -} - -// Parallel illustrates the use of a Group for synchronizing a simple parallel -// task: the "Google Search 2.0" function from -// https://talks.golang.org/2012/concurrency.slide#46, augmented with a Context -// and error-handling. -func ExampleGroup_parallel() { - Google := func(ctx context.Context, query string) ([]Result, error) { - g := WithContext(ctx) - - searches := []Search{Web, Image, Video} - results := make([]Result, len(searches)) - for i, search := range searches { - i, search := i, search // https://golang.org/doc/faq#closures_and_goroutines - g.Go(func(context.Context) error { - result, err := search(ctx, query) - if err == nil { - results[i] = result - } - return err - }) - } - if err := g.Wait(); err != nil { - return nil, err - } - return results, nil - } - - results, err := Google(context.Background(), "golang") - if err != nil { - fmt.Fprintln(os.Stderr, err) - return - } - for _, result := range results { - fmt.Println(result) - } - - // Output: - // web result for "golang" - // image result for "golang" - // video result for "golang" -} - -func TestZeroGroup(t *testing.T) { - err1 := errors.New("errgroup_test: 1") - err2 := errors.New("errgroup_test: 2") - - cases := []struct { - errs []error - }{ - {errs: []error{}}, - {errs: []error{nil}}, - {errs: []error{err1}}, - {errs: []error{err1, nil}}, - {errs: []error{err1, nil, err2}}, - } - - for _, tc := range cases { - var g Group - - var firstErr error - for i, err := range tc.errs { - err := err - g.Go(func(context.Context) error { return err }) - - if firstErr == nil && err != nil { - firstErr = err - } - - if gErr := g.Wait(); gErr != firstErr { - t.Errorf("after g.Go(func() error { return err }) for err in %v\n"+ - "g.Wait() = %v; want %v", tc.errs[:i+1], err, firstErr) - } - } - } -} - -func TestWithCancel(t *testing.T) { - g := WithCancel(context.Background()) - g.Go(func(ctx context.Context) error { - time.Sleep(100 * time.Millisecond) - return fmt.Errorf("boom") - }) - var doneErr error - g.Go(func(ctx context.Context) error { - select { - case <-ctx.Done(): - doneErr = ctx.Err() - } - return doneErr - }) - g.Wait() - if doneErr != context.Canceled { - t.Error("error should be Canceled") - } -} diff --git a/pkg/sync/errgroup/example_test.go b/pkg/sync/errgroup/example_test.go deleted file mode 100644 index e59da4b57..000000000 --- a/pkg/sync/errgroup/example_test.go +++ /dev/null @@ -1,47 +0,0 @@ -package errgroup - -import ( - "context" -) - -func fakeRunTask(ctx context.Context) error { - return nil -} - -func ExampleGroup_group() { - g := Group{} - g.Go(fakeRunTask) - g.Go(fakeRunTask) - if err := g.Wait(); err != nil { - // handle err - } -} - -func ExampleGroup_ctx() { - g := WithContext(context.Background()) - g.Go(fakeRunTask) - g.Go(fakeRunTask) - if err := g.Wait(); err != nil { - // handle err - } -} - -func ExampleGroup_cancel() { - g := WithCancel(context.Background()) - g.Go(fakeRunTask) - g.Go(fakeRunTask) - if err := g.Wait(); err != nil { - // handle err - } -} - -func ExampleGroup_maxproc() { - g := Group{} - // set max concurrency - g.GOMAXPROCS(2) - g.Go(fakeRunTask) - g.Go(fakeRunTask) - if err := g.Wait(); err != nil { - // handle err - } -} diff --git a/pkg/sync/pipeline/CHANGELOG.md b/pkg/sync/pipeline/CHANGELOG.md deleted file mode 100755 index 2120a2356..000000000 --- a/pkg/sync/pipeline/CHANGELOG.md +++ /dev/null @@ -1,9 +0,0 @@ -### pipeline - -#### Version 1.2.0 -> 1. 默认为平滑触发事件 -> 2. 增加metric上报 -#### Version 1.1.0 -> 1. 增加平滑时间的支持 -#### Version 1.0.0 -> 1. 提供聚合方法 内部区分压测流量 diff --git a/pkg/sync/pipeline/README.md b/pkg/sync/pipeline/README.md deleted file mode 100644 index b056c0901..000000000 --- a/pkg/sync/pipeline/README.md +++ /dev/null @@ -1,3 +0,0 @@ -# pkg/sync/pipeline - -提供内存批量聚合工具 diff --git a/pkg/sync/pipeline/fanout/CHANGELOG.md b/pkg/sync/pipeline/fanout/CHANGELOG.md deleted file mode 100755 index c289a6425..000000000 --- a/pkg/sync/pipeline/fanout/CHANGELOG.md +++ /dev/null @@ -1,6 +0,0 @@ -### pipeline/fanout - -#### Version 1.1.0 -> 1. 增加处理速度metric上报 -#### Version 1.0.0 -> 1. library/cache包改为fanout diff --git a/pkg/sync/pipeline/fanout/README.md b/pkg/sync/pipeline/fanout/README.md deleted file mode 100644 index cf88ddd8e..000000000 --- a/pkg/sync/pipeline/fanout/README.md +++ /dev/null @@ -1,14 +0,0 @@ -# pkg/sync/pipeline/fanout - -功能: - -* 支持定义Worker 数量的goroutine,进行消费 -* 内部支持的元数据传递(pkg/net/metadata) - -示例: -```golang -//名称为cache 执行线程为1 buffer长度为1024 -cache := fanout.New("cache", fanout.Worker(1), fanout.Buffer(1024)) -cache.Do(c, func(c context.Context) { SomeFunc(c, args...) }) -cache.Close() -``` \ No newline at end of file diff --git a/pkg/sync/pipeline/fanout/example_test.go b/pkg/sync/pipeline/fanout/example_test.go deleted file mode 100644 index 5de973199..000000000 --- a/pkg/sync/pipeline/fanout/example_test.go +++ /dev/null @@ -1,22 +0,0 @@ -package fanout - -import "context" - -// addCache 加缓存的例子 -func addCache(c context.Context, id, value int) { - // some thing... -} - -func Example() { - // 这里只是举个例子 真正使用的时候 应该用bm/rpc 传过来的context - var c = context.Background() - // 新建一个fanout 对象 名称为cache 名称主要用来上报监控和打日志使用 最好不要重复 - // (可选参数) worker数量为1 表示后台只有1个线程在工作 - // (可选参数) buffer 为1024 表示缓存chan长度为1024 如果chan慢了 再调用Do方法就会报错 设定长度主要为了防止OOM - cache := New("cache", Worker(1), Buffer(1024)) - // 需要异步执行的方法 - // 这里传进来的c里面的meta信息会被复制 超时会忽略 addCache拿到的context已经没有超时信息了 - cache.Do(c, func(c context.Context) { addCache(c, 0, 0) }) - // 程序结束的时候关闭fanout 会等待后台线程完成后返回 - cache.Close() -} diff --git a/pkg/sync/pipeline/fanout/fanout.go b/pkg/sync/pipeline/fanout/fanout.go deleted file mode 100644 index 2253395ce..000000000 --- a/pkg/sync/pipeline/fanout/fanout.go +++ /dev/null @@ -1,150 +0,0 @@ -package fanout - -import ( - "context" - "errors" - "runtime" - "sync" - - "github.com/go-kratos/kratos/pkg/log" - "github.com/go-kratos/kratos/pkg/net/metadata" - "github.com/go-kratos/kratos/pkg/net/trace" -) - -var ( - // ErrFull chan full. - ErrFull = errors.New("fanout: chan full") - traceTags = []trace.Tag{ - {Key: trace.TagSpanKind, Value: "background"}, - {Key: trace.TagComponent, Value: "sync/pipeline/fanout"}, - } -) - -type options struct { - worker int - buffer int -} - -// Option fanout option -type Option func(*options) - -// Worker specifies the worker of fanout -func Worker(n int) Option { - if n <= 0 { - panic("fanout: worker should > 0") - } - return func(o *options) { - o.worker = n - } -} - -// Buffer specifies the buffer of fanout -func Buffer(n int) Option { - if n <= 0 { - panic("fanout: buffer should > 0") - } - return func(o *options) { - o.buffer = n - } -} - -type item struct { - f func(c context.Context) - ctx context.Context -} - -// Fanout async consume data from chan. -type Fanout struct { - name string - ch chan item - options *options - waiter sync.WaitGroup - - ctx context.Context - cancel func() -} - -// New new a fanout struct. -func New(name string, opts ...Option) *Fanout { - if name == "" { - name = "anonymous" - } - o := &options{ - worker: 1, - buffer: 1024, - } - for _, op := range opts { - op(o) - } - c := &Fanout{ - ch: make(chan item, o.buffer), - name: name, - options: o, - } - c.ctx, c.cancel = context.WithCancel(context.Background()) - c.waiter.Add(o.worker) - for i := 0; i < o.worker; i++ { - go c.proc() - } - return c -} - -func (c *Fanout) proc() { - defer c.waiter.Done() - for { - select { - case t := <-c.ch: - wrapFunc(t.f)(t.ctx) - _metricChanSize.Set(float64(len(c.ch)), c.name) - _metricCount.Inc(c.name) - case <-c.ctx.Done(): - return - } - } -} - -func wrapFunc(f func(c context.Context)) (res func(context.Context)) { - res = func(ctx context.Context) { - defer func() { - if r := recover(); r != nil { - buf := make([]byte, 64*1024) - buf = buf[:runtime.Stack(buf, false)] - log.Error("panic in fanout proc, err: %s, stack: %s", r, buf) - } - }() - f(ctx) - if tr, ok := trace.FromContext(ctx); ok { - tr.Finish(nil) - } - } - return -} - -// Do save a callback func. -func (c *Fanout) Do(ctx context.Context, f func(ctx context.Context)) (err error) { - if f == nil || c.ctx.Err() != nil { - return c.ctx.Err() - } - nakeCtx := metadata.WithContext(ctx) - if tr, ok := trace.FromContext(ctx); ok { - tr = tr.Fork("", "Fanout:Do").SetTag(traceTags...) - nakeCtx = trace.NewContext(nakeCtx, tr) - } - select { - case c.ch <- item{f: f, ctx: nakeCtx}: - default: - err = ErrFull - } - _metricChanSize.Set(float64(len(c.ch)), c.name) - return -} - -// Close close fanout -func (c *Fanout) Close() error { - if err := c.ctx.Err(); err != nil { - return err - } - c.cancel() - c.waiter.Wait() - return nil -} diff --git a/pkg/sync/pipeline/fanout/fanout_test.go b/pkg/sync/pipeline/fanout/fanout_test.go deleted file mode 100644 index ef1df1169..000000000 --- a/pkg/sync/pipeline/fanout/fanout_test.go +++ /dev/null @@ -1,30 +0,0 @@ -package fanout - -import ( - "context" - "testing" - "time" -) - -func TestFanout_Do(t *testing.T) { - ca := New("cache", Worker(1), Buffer(1024)) - var run bool - ca.Do(context.Background(), func(c context.Context) { - run = true - panic("error") - }) - time.Sleep(time.Millisecond * 50) - t.Log("not panic") - if !run { - t.Fatal("expect run be true") - } -} - -func TestFanout_Close(t *testing.T) { - ca := New("cache", Worker(1), Buffer(1024)) - ca.Close() - err := ca.Do(context.Background(), func(c context.Context) {}) - if err == nil { - t.Fatal("expect get err") - } -} diff --git a/pkg/sync/pipeline/fanout/metrics.go b/pkg/sync/pipeline/fanout/metrics.go deleted file mode 100644 index 6a7207326..000000000 --- a/pkg/sync/pipeline/fanout/metrics.go +++ /dev/null @@ -1,27 +0,0 @@ -package fanout - -import ( - "github.com/go-kratos/kratos/pkg/stat/metric" -) - -const ( - _metricNamespace = "sync" - _metricSubSystem = "pipeline_fanout" -) - -var ( - _metricChanSize = metric.NewGaugeVec(&metric.GaugeVecOpts{ - Namespace: _metricNamespace, - Subsystem: _metricSubSystem, - Name: "chan_len", - Help: "sync pipeline fanout current channel size.", - Labels: []string{"name"}, - }) - _metricCount = metric.NewCounterVec(&metric.CounterVecOpts{ - Namespace: _metricNamespace, - Subsystem: _metricSubSystem, - Name: "process_count", - Help: "process count", - Labels: []string{"name"}, - }) -) diff --git a/pkg/sync/pipeline/pipeline.go b/pkg/sync/pipeline/pipeline.go deleted file mode 100644 index 7347e5f6c..000000000 --- a/pkg/sync/pipeline/pipeline.go +++ /dev/null @@ -1,223 +0,0 @@ -package pipeline - -import ( - "context" - "errors" - "strconv" - "sync" - "time" - - "github.com/go-kratos/kratos/pkg/net/metadata" - "github.com/go-kratos/kratos/pkg/stat/metric" - xtime "github.com/go-kratos/kratos/pkg/time" -) - -// ErrFull channel full error -var ErrFull = errors.New("channel full") - -const _metricNamespace = "sync" -const _metricSubSystem = "pipeline" - -var ( - _metricCount = metric.NewCounterVec(&metric.CounterVecOpts{ - Namespace: _metricNamespace, - Subsystem: _metricSubSystem, - Name: "process_count", - Help: "process count", - Labels: []string{"name", "chan"}, - }) - _metricChanLen = metric.NewGaugeVec(&metric.GaugeVecOpts{ - Namespace: _metricNamespace, - Subsystem: _metricSubSystem, - Name: "chan_len", - Help: "channel length", - Labels: []string{"name", "chan"}, - }) -) - -type message struct { - key string - value interface{} -} - -// Pipeline pipeline struct -type Pipeline struct { - Do func(c context.Context, index int, values map[string][]interface{}) - Split func(key string) int - chans []chan *message - mirrorChans []chan *message - config *Config - wait sync.WaitGroup - name string -} - -// Config Pipeline config -type Config struct { - // MaxSize merge size - MaxSize int - // Interval merge interval - Interval xtime.Duration - // Buffer channel size - Buffer int - // Worker channel number - Worker int - // Name use for metrics - Name string -} - -func (c *Config) fix() { - if c.MaxSize <= 0 { - c.MaxSize = 1000 - } - if c.Interval <= 0 { - c.Interval = xtime.Duration(time.Second) - } - if c.Buffer <= 0 { - c.Buffer = 1000 - } - if c.Worker <= 0 { - c.Worker = 10 - } - if c.Name == "" { - c.Name = "anonymous" - } -} - -// NewPipeline new pipline -func NewPipeline(config *Config) (res *Pipeline) { - if config == nil { - config = &Config{} - } - config.fix() - res = &Pipeline{ - chans: make([]chan *message, config.Worker), - mirrorChans: make([]chan *message, config.Worker), - config: config, - name: config.Name, - } - for i := 0; i < config.Worker; i++ { - res.chans[i] = make(chan *message, config.Buffer) - res.mirrorChans[i] = make(chan *message, config.Buffer) - } - return -} - -// Start start all mergeproc -func (p *Pipeline) Start() { - if p.Do == nil { - panic("pipeline: do func is nil") - } - if p.Split == nil { - panic("pipeline: split func is nil") - } - var mirror bool - p.wait.Add(len(p.chans) + len(p.mirrorChans)) - for i, ch := range p.chans { - go p.mergeproc(mirror, i, ch) - } - mirror = true - for i, ch := range p.mirrorChans { - go p.mergeproc(mirror, i, ch) - } -} - -// SyncAdd sync add a value to channal, channel shard in split method -func (p *Pipeline) SyncAdd(c context.Context, key string, value interface{}) (err error) { - ch, msg := p.add(c, key, value) - select { - case ch <- msg: - case <-c.Done(): - err = c.Err() - } - return -} - -// Add async add a value to channal, channel shard in split method -func (p *Pipeline) Add(c context.Context, key string, value interface{}) (err error) { - ch, msg := p.add(c, key, value) - select { - case ch <- msg: - default: - err = ErrFull - } - return -} - -func (p *Pipeline) add(c context.Context, key string, value interface{}) (ch chan *message, m *message) { - shard := p.Split(key) % p.config.Worker - if metadata.String(c, metadata.Mirror) != "" { - ch = p.mirrorChans[shard] - } else { - ch = p.chans[shard] - } - m = &message{key: key, value: value} - return -} - -// Close all goroutinue -func (p *Pipeline) Close() (err error) { - for _, ch := range p.chans { - ch <- nil - } - for _, ch := range p.mirrorChans { - ch <- nil - } - p.wait.Wait() - return -} - -func (p *Pipeline) mergeproc(mirror bool, index int, ch <-chan *message) { - defer p.wait.Done() - var ( - m *message - vals = make(map[string][]interface{}, p.config.MaxSize) - closed bool - count int - inteval = p.config.Interval - timeout = false - ) - if index > 0 { - inteval = xtime.Duration(int64(index) * (int64(p.config.Interval) / int64(p.config.Worker))) - } - timer := time.NewTimer(time.Duration(inteval)) - defer timer.Stop() - for { - select { - case m = <-ch: - if m == nil { - closed = true - break - } - count++ - vals[m.key] = append(vals[m.key], m.value) - if count >= p.config.MaxSize { - break - } - continue - case <-timer.C: - timeout = true - } - name := p.name - process := count - if len(vals) > 0 { - ctx := context.Background() - if mirror { - ctx = metadata.NewContext(ctx, metadata.MD{metadata.Mirror: "1"}) - name = "mirror_" + name - } - p.Do(ctx, index, vals) - vals = make(map[string][]interface{}, p.config.MaxSize) - count = 0 - } - _metricChanLen.Set(float64(len(ch)), name, strconv.Itoa(index)) - _metricCount.Add(float64(process), name, strconv.Itoa(index)) - if closed { - return - } - if !timer.Stop() && !timeout { - <-timer.C - timeout = false - } - timer.Reset(time.Duration(p.config.Interval)) - } -} diff --git a/pkg/sync/pipeline/pipeline_test.go b/pkg/sync/pipeline/pipeline_test.go deleted file mode 100644 index ea9d61838..000000000 --- a/pkg/sync/pipeline/pipeline_test.go +++ /dev/null @@ -1,131 +0,0 @@ -package pipeline - -import ( - "context" - "reflect" - "strconv" - "testing" - "time" - - "github.com/go-kratos/kratos/pkg/net/metadata" - xtime "github.com/go-kratos/kratos/pkg/time" -) - -func TestPipeline(t *testing.T) { - conf := &Config{ - MaxSize: 3, - Interval: xtime.Duration(time.Millisecond * 20), - Buffer: 3, - Worker: 10, - } - type recv struct { - mirror string - ch int - values map[string][]interface{} - } - var runs []recv - do := func(c context.Context, ch int, values map[string][]interface{}) { - runs = append(runs, recv{ - mirror: metadata.String(c, metadata.Mirror), - values: values, - ch: ch, - }) - } - split := func(s string) int { - n, _ := strconv.Atoi(s) - return n - } - p := NewPipeline(conf) - p.Do = do - p.Split = split - p.Start() - p.Add(context.Background(), "1", 1) - p.Add(context.Background(), "1", 2) - p.Add(context.Background(), "11", 3) - p.Add(context.Background(), "2", 3) - time.Sleep(time.Millisecond * 60) - mirrorCtx := metadata.NewContext(context.Background(), metadata.MD{metadata.Mirror: "1"}) - p.Add(mirrorCtx, "2", 3) - time.Sleep(time.Millisecond * 60) - p.SyncAdd(mirrorCtx, "5", 5) - time.Sleep(time.Millisecond * 60) - p.Close() - expt := []recv{ - { - mirror: "", - ch: 1, - values: map[string][]interface{}{ - "1": {1, 2}, - "11": {3}, - }, - }, - { - mirror: "", - ch: 2, - values: map[string][]interface{}{ - "2": {3}, - }, - }, - { - mirror: "1", - ch: 2, - values: map[string][]interface{}{ - "2": {3}, - }, - }, - { - mirror: "1", - ch: 5, - values: map[string][]interface{}{ - "5": {5}, - }, - }, - } - if !reflect.DeepEqual(runs, expt) { - t.Errorf("expect get %+v,\n got: %+v", expt, runs) - } -} - -func TestPipelineSmooth(t *testing.T) { - conf := &Config{ - MaxSize: 100, - Interval: xtime.Duration(time.Second), - Buffer: 100, - Worker: 10, - } - type result struct { - index int - ts time.Time - } - var results []result - do := func(c context.Context, index int, values map[string][]interface{}) { - results = append(results, result{ - index: index, - ts: time.Now(), - }) - } - split := func(s string) int { - n, _ := strconv.Atoi(s) - return n - } - p := NewPipeline(conf) - p.Do = do - p.Split = split - p.Start() - for i := 0; i < 10; i++ { - p.Add(context.Background(), strconv.Itoa(i), 1) - } - time.Sleep(time.Millisecond * 1500) - if len(results) != conf.Worker { - t.Errorf("expect results equal worker") - t.FailNow() - } - for i, r := range results { - if i > 0 { - if r.ts.Sub(results[i-1].ts) < time.Millisecond*20 { - t.Errorf("expect runs be smooth") - t.FailNow() - } - } - } -} diff --git a/pkg/testing/lich/README.md b/pkg/testing/lich/README.md deleted file mode 100644 index 541c6c681..000000000 --- a/pkg/testing/lich/README.md +++ /dev/null @@ -1,4 +0,0 @@ -## testing/lich 运行环境构建 -基于 docker-compose 实现跨平台跨语言环境的容器依赖管理方案,以解决运行ut场景下的 (mysql, redis, mc)容器依赖问题。 - -使用说明参见:https://go-kratos.github.io/kratos/#/ut \ No newline at end of file diff --git a/pkg/testing/lich/composer.go b/pkg/testing/lich/composer.go deleted file mode 100644 index 21ac82d07..000000000 --- a/pkg/testing/lich/composer.go +++ /dev/null @@ -1,129 +0,0 @@ -package lich - -import ( - "bytes" - "crypto/md5" - "encoding/json" - "flag" - "fmt" - "os" - "os/exec" - "path/filepath" - "runtime" - "time" - - "github.com/go-kratos/kratos/pkg/log" -) - -var ( - retry int - noDown bool - yamlPath string - pathHash string - services map[string]*Container -) - -func init() { - flag.StringVar(&yamlPath, "f", "docker-compose.yaml", "composer yaml path.") - flag.IntVar(&retry, "retry", 5, "number of retries on network failure.") - flag.BoolVar(&noDown, "nodown", false, "containers are not recycled.") -} - -func runCompose(args ...string) (output []byte, err error) { - if _, err = os.Stat(yamlPath); os.IsNotExist(err) { - log.Error("os.Stat(%s) composer yaml is not exist!", yamlPath) - return - } - if yamlPath, err = filepath.Abs(yamlPath); err != nil { - log.Error("filepath.Abs(%s) error(%v)", yamlPath, err) - return - } - pathHash = fmt.Sprintf("%x", md5.Sum([]byte(yamlPath)))[:9] - args = append([]string{"-f", yamlPath, "-p", pathHash}, args...) - if output, err = exec.Command("docker-compose", args...).CombinedOutput(); err != nil { - log.Error("exec.Command(docker-compose) args(%v) stdout(%s) error(%v)", args, string(output), err) - return - } - return -} - -// Setup setup UT related environment dependence for everything. -func Setup() (err error) { - if _, err = runCompose("up", "-d"); err != nil { - return - } - defer func() { - if err != nil { - Teardown() - } - }() - if _, err = getServices(); err != nil { - return - } - _, err = checkServices() - return -} - -// Teardown unsetup all environment dependence. -func Teardown() (err error) { - if !noDown { - _, err = runCompose("down") - } - return -} - -func getServices() (output []byte, err error) { - if output, err = runCompose("config", "--services"); err != nil { - return - } - var eol = []byte("\n") - if output = bytes.TrimSpace(output); runtime.GOOS == "windows" { - eol = []byte("\r\n") - } - services = make(map[string]*Container) - for _, svr := range bytes.Split(output, eol) { - if output, err = runCompose("ps", "-q", string(svr)); err != nil { - return - } - var ( - id = string(bytes.TrimSpace(output)) - args = []string{"inspect", id, "--format", "'{{json .}}'"} - ) - if output, err = exec.Command("docker", args...).CombinedOutput(); err != nil { - log.Error("exec.Command(docker) args(%v) stdout(%s) error(%v)", args, string(output), err) - return - } - if output = bytes.TrimSpace(output); bytes.Equal(output, []byte("")) { - err = fmt.Errorf("service: %s | container: %s fails to launch", svr, id) - log.Error("exec.Command(docker) args(%v) error(%v)", args, err) - return - } - var c = &Container{} - if err = json.Unmarshal(bytes.Trim(output, "'"), c); err != nil { - log.Error("json.Unmarshal(%s) error(%v)", string(output), err) - return - } - services[string(svr)] = c - } - return -} - -func checkServices() (output []byte, err error) { - defer func() { - if err != nil && retry > 0 { - retry-- - getServices() - time.Sleep(time.Second * 5) - output, err = checkServices() - return - } - }() - for svr, c := range services { - if err = c.Healthcheck(); err != nil { - log.Error("healthcheck(%s) error(%v) retrying %d times...", svr, err, retry) - return - } - // TODO About container check and more... - } - return -} diff --git a/pkg/testing/lich/healthcheck.go b/pkg/testing/lich/healthcheck.go deleted file mode 100644 index 68bc758ee..000000000 --- a/pkg/testing/lich/healthcheck.go +++ /dev/null @@ -1,85 +0,0 @@ -package lich - -import ( - "database/sql" - "fmt" - "net" - "strconv" - "strings" - - "github.com/go-kratos/kratos/pkg/log" - // Register go-sql-driver stuff - _ "github.com/go-sql-driver/mysql" -) - -var healthchecks = map[string]func(*Container) error{"mysql": checkMysql, "mariadb": checkMysql} - -// Healthcheck check container health. -func (c *Container) Healthcheck() (err error) { - if status, health := c.State.Status, c.State.Health.Status; !c.State.Running || (health != "" && health != "healthy") { - err = fmt.Errorf("service: %s | container: %s not running", c.GetImage(), c.GetID()) - log.Error("docker status(%s) health(%s) error(%v)", status, health, err) - return - } - if check, ok := healthchecks[c.GetImage()]; ok { - err = check(c) - return - } - for proto, ports := range c.NetworkSettings.Ports { - if id := c.GetID(); !strings.Contains(proto, "tcp") { - log.Error("container: %s proto(%s) unsupported.", id, proto) - continue - } - for _, publish := range ports { - var ( - ip = net.ParseIP(publish.HostIP) - port, _ = strconv.Atoi(publish.HostPort) - tcpAddr = &net.TCPAddr{IP: ip, Port: port} - tcpConn *net.TCPConn - ) - if tcpConn, err = net.DialTCP("tcp", nil, tcpAddr); err != nil { - log.Error("net.DialTCP(%s:%s) error(%v)", publish.HostIP, publish.HostPort, err) - return - } - tcpConn.Close() - } - } - return -} - -func checkMysql(c *Container) (err error) { - var ip, port, user, passwd string - for _, env := range c.Config.Env { - splits := strings.Split(env, "=") - if strings.Contains(splits[0], "MYSQL_ROOT_PASSWORD") { - user, passwd = "root", splits[1] - continue - } - if strings.Contains(splits[0], "MYSQL_ALLOW_EMPTY_PASSWORD") { - user, passwd = "root", "" - continue - } - if strings.Contains(splits[0], "MYSQL_USER") { - user = splits[1] - continue - } - if strings.Contains(splits[0], "MYSQL_PASSWORD") { - passwd = splits[1] - continue - } - } - var db *sql.DB - if ports, ok := c.NetworkSettings.Ports["3306/tcp"]; ok { - ip, port = ports[0].HostIP, ports[0].HostPort - } - var dsn = fmt.Sprintf("%s:%s@tcp(%s:%s)/", user, passwd, ip, port) - if db, err = sql.Open("mysql", dsn); err != nil { - log.Error("sql.Open(mysql) dsn(%s) error(%v)", dsn, err) - return - } - if err = db.Ping(); err != nil { - log.Error("ping(db) dsn(%s) error(%v)", dsn, err) - } - defer db.Close() - return -} diff --git a/pkg/testing/lich/model.go b/pkg/testing/lich/model.go deleted file mode 100644 index 64653a681..000000000 --- a/pkg/testing/lich/model.go +++ /dev/null @@ -1,88 +0,0 @@ -package lich - -import ( - "strings" - "time" -) - -// Container docker inspect resp. -type Container struct { - ID string `json:"Id"` - Created time.Time `json:"Created"` - Path string `json:"Path"` - Args []string `json:"Args"` - State struct { - Status string `json:"Status"` - Running bool `json:"Running"` - Paused bool `json:"Paused"` - Restarting bool `json:"Restarting"` - OOMKilled bool `json:"OOMKilled"` - Dead bool `json:"Dead"` - Pid int `json:"Pid"` - ExitCode int `json:"ExitCode"` - Error string `json:"Error"` - StartedAt time.Time `json:"StartedAt"` - FinishedAt time.Time `json:"FinishedAt"` - Health struct { - Status string `json:"Status"` - FailingStreak int `json:"FailingStreak"` - Log []struct { - Start time.Time `json:"Start"` - End time.Time `json:"End"` - ExitCode int `json:"ExitCode"` - Output string `json:"Output"` - } `json:"Log"` - } `json:"Health"` - } `json:"State"` - Config struct { - Hostname string `json:"Hostname"` - Domainname string `json:"Domainname"` - User string `json:"User"` - Tty bool `json:"Tty"` - OpenStdin bool `json:"OpenStdin"` - StdinOnce bool `json:"StdinOnce"` - Env []string `json:"Env"` - Cmd []string `json:"Cmd"` - Image string `json:"Image"` - WorkingDir string `json:"WorkingDir"` - Entrypoint []string `json:"Entrypoint"` - } `json:"Config"` - Image string `json:"Image"` - ResolvConfPath string `json:"ResolvConfPath"` - HostnamePath string `json:"HostnamePath"` - HostsPath string `json:"HostsPath"` - LogPath string `json:"LogPath"` - Name string `json:"Name"` - RestartCount int `json:"RestartCount"` - Driver string `json:"Driver"` - Platform string `json:"Platform"` - MountLabel string `json:"MountLabel"` - ProcessLabel string `json:"ProcessLabel"` - AppArmorProfile string `json:"AppArmorProfile"` - NetworkSettings struct { - Bridge string `json:"Bridge"` - SandboxID string `json:"SandboxID"` - HairpinMode bool `json:"HairpinMode"` - Ports map[string][]struct { - HostIP string `json:"HostIp"` - HostPort string `json:"HostPort"` - } `json:"Ports"` - } `json:"NetworkSettings"` -} - -// GetImage get image name at container -func (c *Container) GetImage() (image string) { - image = c.Config.Image - if images := strings.Split(image, ":"); len(images) > 0 { - image = images[0] - } - return -} - -// GetID get id at container -func (c *Container) GetID() (id string) { - if id = c.ID; len(id) > 9 { - id = id[0:9] - } - return -} diff --git a/pkg/time/README.md b/pkg/time/README.md deleted file mode 100644 index 6e099f312..000000000 --- a/pkg/time/README.md +++ /dev/null @@ -1,5 +0,0 @@ -# time - -## 项目简介 - -Kratos的时间模块,主要用于mysql时间戳转换、配置文件读取并转换、Context超时时间比较 diff --git a/pkg/time/time.go b/pkg/time/time.go deleted file mode 100644 index cb03e9919..000000000 --- a/pkg/time/time.go +++ /dev/null @@ -1,59 +0,0 @@ -package time - -import ( - "context" - "database/sql/driver" - "strconv" - xtime "time" -) - -// Time be used to MySql timestamp converting. -type Time int64 - -// Scan scan time. -func (jt *Time) Scan(src interface{}) (err error) { - switch sc := src.(type) { - case xtime.Time: - *jt = Time(sc.Unix()) - case string: - var i int64 - i, err = strconv.ParseInt(sc, 10, 64) - *jt = Time(i) - } - return -} - -// Value get time value. -func (jt Time) Value() (driver.Value, error) { - return xtime.Unix(int64(jt), 0), nil -} - -// Time get time. -func (jt Time) Time() xtime.Time { - return xtime.Unix(int64(jt), 0) -} - -// Duration be used toml unmarshal string time, like 1s, 500ms. -type Duration xtime.Duration - -// UnmarshalText unmarshal text to duration. -func (d *Duration) UnmarshalText(text []byte) error { - tmp, err := xtime.ParseDuration(string(text)) - if err == nil { - *d = Duration(tmp) - } - return err -} - -// Shrink will decrease the duration by comparing with context's timeout duration -// and return new timeout\context\CancelFunc. -func (d Duration) Shrink(c context.Context) (Duration, context.Context, context.CancelFunc) { - if deadline, ok := c.Deadline(); ok { - if ctimeout := xtime.Until(deadline); ctimeout < xtime.Duration(d) { - // deliver small timeout - return Duration(ctimeout), c, func() {} - } - } - ctx, cancel := context.WithTimeout(c, xtime.Duration(d)) - return d, ctx, cancel -} diff --git a/pkg/time/time_test.go b/pkg/time/time_test.go deleted file mode 100644 index f0c2d6604..000000000 --- a/pkg/time/time_test.go +++ /dev/null @@ -1,60 +0,0 @@ -package time - -import ( - "context" - "testing" - "time" -) - -func TestShrink(t *testing.T) { - var d Duration - err := d.UnmarshalText([]byte("1s")) - if err != nil { - t.Fatalf("TestShrink: d.UnmarshalText failed!err:=%v", err) - } - c := context.Background() - to, ctx, cancel := d.Shrink(c) - defer cancel() - if time.Duration(to) != time.Second { - t.Fatalf("new timeout must be equal 1 second") - } - if deadline, ok := ctx.Deadline(); !ok || time.Until(deadline) > time.Second || time.Until(deadline) < time.Millisecond*500 { - t.Fatalf("ctx deadline must be less than 1s and greater than 500ms") - } -} - -func TestShrinkWithTimeout(t *testing.T) { - var d Duration - err := d.UnmarshalText([]byte("1s")) - if err != nil { - t.Fatalf("TestShrink: d.UnmarshalText failed!err:=%v", err) - } - c, cancel := context.WithTimeout(context.Background(), time.Second*2) - defer cancel() - to, ctx, cancel := d.Shrink(c) - defer cancel() - if time.Duration(to) != time.Second { - t.Fatalf("new timeout must be equal 1 second") - } - if deadline, ok := ctx.Deadline(); !ok || time.Until(deadline) > time.Second || time.Until(deadline) < time.Millisecond*500 { - t.Fatalf("ctx deadline must be less than 1s and greater than 500ms") - } -} - -func TestShrinkWithDeadline(t *testing.T) { - var d Duration - err := d.UnmarshalText([]byte("1s")) - if err != nil { - t.Fatalf("TestShrink: d.UnmarshalText failed!err:=%v", err) - } - c, cancel := context.WithTimeout(context.Background(), time.Millisecond*500) - defer cancel() - to, ctx, cancel := d.Shrink(c) - defer cancel() - if time.Duration(to) >= time.Millisecond*500 { - t.Fatalf("new timeout must be less than 500 ms") - } - if deadline, ok := ctx.Deadline(); !ok || time.Until(deadline) > time.Millisecond*500 || time.Until(deadline) < time.Millisecond*200 { - t.Fatalf("ctx deadline must be less than 500ms and greater than 200ms") - } -} diff --git a/registry/registry.go b/registry/registry.go new file mode 100644 index 000000000..cd74ca694 --- /dev/null +++ b/registry/registry.go @@ -0,0 +1,41 @@ +package registry + +// Registry is service registry. +type Registry interface { + // Register the registration. + Register(service *ServiceInstance) error + // Deregister the registration. + Deregister(service *ServiceInstance) error + // Service return the service instances in memory according to the service name. + Service(name string) ([]*ServiceInstance, error) + // Watch creates a watcher according to the service name. + Watch(name string) (Watcher, error) +} + +// Watcher is service watcher. +type Watcher interface { + // Watch returns services in the following two cases: + // 1.the first time to watch and the service instance list is not empty. + // 2.any service instance changes found. + // if the above two conditions are not met, it will block until context deadline exceeded or canceled + Next() ([]*ServiceInstance, error) + // Close close the watcher. + Close() error +} + +// ServiceInstance is an instance of a service in a discovery system. +type ServiceInstance struct { + // ID is the unique instance ID as registered. + ID string + // Name is the service name as registered. + Name string + // Version is the version of the compiled. + Version string + // Metadata is the kv pair metadata associated with the service instance. + Metadata map[string]string + // Endpoints is endpoint addresses of the service instance. + // schema: + // http://127.0.0.1:8000?isSecure=false + // grpc://127.0.0.1:9000?isSecure=false + Endpoints []string +} diff --git a/third_party/CHANGELOG.md b/third_party/CHANGELOG.md deleted file mode 100644 index 99f2e38bc..000000000 --- a/third_party/CHANGELOG.md +++ /dev/null @@ -1,7 +0,0 @@ -#### Version 1.0.1 - -> 1.gogo.protobuf迁移到third_party/github目录下 - -#### Version 1.0.0 - -> 1.添加 gogo proto diff --git a/third_party/README.md b/third_party/README.md new file mode 100644 index 000000000..005faa2cf --- /dev/null +++ b/third_party/README.md @@ -0,0 +1 @@ +# third_party diff --git a/third_party/github.com/gogo/protobuf/AUTHORS b/third_party/github.com/gogo/protobuf/AUTHORS deleted file mode 100644 index 3d97fc7a2..000000000 --- a/third_party/github.com/gogo/protobuf/AUTHORS +++ /dev/null @@ -1,15 +0,0 @@ -# This is the official list of GoGo authors for copyright purposes. -# This file is distinct from the CONTRIBUTORS file, which -# lists people. For example, employees are listed in CONTRIBUTORS, -# but not in AUTHORS, because the employer holds the copyright. - -# Names should be added to this file as one of -# Organization's name -# Individual's name -# Individual's name - -# Please keep the list sorted. - -Sendgrid, Inc -Vastech SA (PTY) LTD -Walter Schulze diff --git a/third_party/github.com/gogo/protobuf/CONTRIBUTORS b/third_party/github.com/gogo/protobuf/CONTRIBUTORS deleted file mode 100644 index 1b4f6c208..000000000 --- a/third_party/github.com/gogo/protobuf/CONTRIBUTORS +++ /dev/null @@ -1,23 +0,0 @@ -Anton Povarov -Brian Goff -Clayton Coleman -Denis Smirnov -DongYun Kang -Dwayne Schultz -Georg Apitz -Gustav Paul -Johan Brandhorst -John Shahid -John Tuley -Laurent -Patrick Lee -Peter Edge -Roger Johansson -Sam Nguyen -Sergio Arbeo -Stephen J Day -Tamir Duberstein -Todd Eisenberger -Tormod Erevik Lea -Vyacheslav Kim -Walter Schulze diff --git a/third_party/github.com/gogo/protobuf/LICENSE b/third_party/github.com/gogo/protobuf/LICENSE deleted file mode 100644 index f57de90da..000000000 --- a/third_party/github.com/gogo/protobuf/LICENSE +++ /dev/null @@ -1,35 +0,0 @@ -Copyright (c) 2013, The GoGo Authors. All rights reserved. - -Protocol Buffers for Go with Gadgets - -Go support for Protocol Buffers - Google's data interchange format - -Copyright 2010 The Go Authors. All rights reserved. -https://github.com/golang/protobuf - -Redistribution and use in source and binary forms, with or without -modification, are permitted provided that the following conditions are -met: - - * Redistributions of source code must retain the above copyright -notice, this list of conditions and the following disclaimer. - * Redistributions in binary form must reproduce the above -copyright notice, this list of conditions and the following disclaimer -in the documentation and/or other materials provided with the -distribution. - * Neither the name of Google Inc. nor the names of its -contributors may be used to endorse or promote products derived from -this software without specific prior written permission. - -THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS -"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT -LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR -A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT -OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, -DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY -THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT -(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE -OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - diff --git a/third_party/github.com/gogo/protobuf/gogoproto/gogo.proto b/third_party/github.com/gogo/protobuf/gogoproto/gogo.proto deleted file mode 100644 index b80c85653..000000000 --- a/third_party/github.com/gogo/protobuf/gogoproto/gogo.proto +++ /dev/null @@ -1,144 +0,0 @@ -// Protocol Buffers for Go with Gadgets -// -// Copyright (c) 2013, The GoGo Authors. All rights reserved. -// http://github.com/gogo/protobuf -// -// Redistribution and use in source and binary forms, with or without -// modification, are permitted provided that the following conditions are -// met: -// -// * Redistributions of source code must retain the above copyright -// notice, this list of conditions and the following disclaimer. -// * Redistributions in binary form must reproduce the above -// copyright notice, this list of conditions and the following disclaimer -// in the documentation and/or other materials provided with the -// distribution. -// -// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS -// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT -// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR -// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT -// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, -// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY -// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT -// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE -// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - -syntax = "proto2"; -package gogoproto; - -import "google/protobuf/descriptor.proto"; - -option java_package = "com.google.protobuf"; -option java_outer_classname = "GoGoProtos"; -option go_package = "github.com/gogo/protobuf/gogoproto"; - -extend google.protobuf.EnumOptions { - optional bool goproto_enum_prefix = 62001; - optional bool goproto_enum_stringer = 62021; - optional bool enum_stringer = 62022; - optional string enum_customname = 62023; - optional bool enumdecl = 62024; -} - -extend google.protobuf.EnumValueOptions { - optional string enumvalue_customname = 66001; -} - -extend google.protobuf.FileOptions { - optional bool goproto_getters_all = 63001; - optional bool goproto_enum_prefix_all = 63002; - optional bool goproto_stringer_all = 63003; - optional bool verbose_equal_all = 63004; - optional bool face_all = 63005; - optional bool gostring_all = 63006; - optional bool populate_all = 63007; - optional bool stringer_all = 63008; - optional bool onlyone_all = 63009; - - optional bool equal_all = 63013; - optional bool description_all = 63014; - optional bool testgen_all = 63015; - optional bool benchgen_all = 63016; - optional bool marshaler_all = 63017; - optional bool unmarshaler_all = 63018; - optional bool stable_marshaler_all = 63019; - - optional bool sizer_all = 63020; - - optional bool goproto_enum_stringer_all = 63021; - optional bool enum_stringer_all = 63022; - - optional bool unsafe_marshaler_all = 63023; - optional bool unsafe_unmarshaler_all = 63024; - - optional bool goproto_extensions_map_all = 63025; - optional bool goproto_unrecognized_all = 63026; - optional bool gogoproto_import = 63027; - optional bool protosizer_all = 63028; - optional bool compare_all = 63029; - optional bool typedecl_all = 63030; - optional bool enumdecl_all = 63031; - - optional bool goproto_registration = 63032; - optional bool messagename_all = 63033; - - optional bool goproto_sizecache_all = 63034; - optional bool goproto_unkeyed_all = 63035; -} - -extend google.protobuf.MessageOptions { - optional bool goproto_getters = 64001; - optional bool goproto_stringer = 64003; - optional bool verbose_equal = 64004; - optional bool face = 64005; - optional bool gostring = 64006; - optional bool populate = 64007; - optional bool stringer = 67008; - optional bool onlyone = 64009; - - optional bool equal = 64013; - optional bool description = 64014; - optional bool testgen = 64015; - optional bool benchgen = 64016; - optional bool marshaler = 64017; - optional bool unmarshaler = 64018; - optional bool stable_marshaler = 64019; - - optional bool sizer = 64020; - - optional bool unsafe_marshaler = 64023; - optional bool unsafe_unmarshaler = 64024; - - optional bool goproto_extensions_map = 64025; - optional bool goproto_unrecognized = 64026; - - optional bool protosizer = 64028; - optional bool compare = 64029; - - optional bool typedecl = 64030; - - optional bool messagename = 64033; - - optional bool goproto_sizecache = 64034; - optional bool goproto_unkeyed = 64035; -} - -extend google.protobuf.FieldOptions { - optional bool nullable = 65001; - optional bool embed = 65002; - optional string customtype = 65003; - optional string customname = 65004; - optional string jsontag = 65005; - optional string moretags = 65006; - optional string casttype = 65007; - optional string castkey = 65008; - optional string castvalue = 65009; - - optional bool stdtime = 65010; - optional bool stdduration = 65011; - optional bool wktpointer = 65012; - -} diff --git a/third_party/google/api/README.md b/third_party/google/api/README.md deleted file mode 100644 index eafe58802..000000000 --- a/third_party/google/api/README.md +++ /dev/null @@ -1,5 +0,0 @@ -This folder contains the schema of the configuration model for the API services -platform. - -**Note**: Protos under this directory are in Alpha status, and therefore are -subject to breaking changes. diff --git a/third_party/google/api/auth.proto b/third_party/google/api/auth.proto deleted file mode 100644 index 7c5d61666..000000000 --- a/third_party/google/api/auth.proto +++ /dev/null @@ -1,184 +0,0 @@ -// Copyright 2019 Google LLC. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -// - -syntax = "proto3"; - -package google.api; - -option go_package = "google.golang.org/genproto/googleapis/api/serviceconfig;serviceconfig"; -option java_multiple_files = true; -option java_outer_classname = "AuthProto"; -option java_package = "com.google.api"; -option objc_class_prefix = "GAPI"; - -// `Authentication` defines the authentication configuration for an API. -// -// Example for an API targeted for external use: -// -// name: calendar.googleapis.com -// authentication: -// providers: -// - id: google_calendar_auth -// jwks_uri: https://www.googleapis.com/oauth2/v1/certs -// issuer: https://securetoken.google.com -// rules: -// - selector: "*" -// requirements: -// provider_id: google_calendar_auth -message Authentication { - // A list of authentication rules that apply to individual API methods. - // - // **NOTE:** All service configuration rules follow "last one wins" order. - repeated AuthenticationRule rules = 3; - - // Defines a set of authentication providers that a service supports. - repeated AuthProvider providers = 4; -} - -// Authentication rules for the service. -// -// By default, if a method has any authentication requirements, every request -// must include a valid credential matching one of the requirements. -// It's an error to include more than one kind of credential in a single -// request. -// -// If a method doesn't have any auth requirements, request credentials will be -// ignored. -message AuthenticationRule { - // Selects the methods to which this rule applies. - // - // Refer to [selector][google.api.DocumentationRule.selector] for syntax details. - string selector = 1; - - // The requirements for OAuth credentials. - OAuthRequirements oauth = 2; - - // If true, the service accepts API keys without any other credential. - bool allow_without_credential = 5; - - // Requirements for additional authentication providers. - repeated AuthRequirement requirements = 7; -} - -// Configuration for an authentication provider, including support for -// [JSON Web Token -// (JWT)](https://tools.ietf.org/html/draft-ietf-oauth-json-web-token-32). -message AuthProvider { - // The unique identifier of the auth provider. It will be referred to by - // `AuthRequirement.provider_id`. - // - // Example: "bookstore_auth". - string id = 1; - - // Identifies the principal that issued the JWT. See - // https://tools.ietf.org/html/draft-ietf-oauth-json-web-token-32#section-4.1.1 - // Usually a URL or an email address. - // - // Example: https://securetoken.google.com - // Example: 1234567-compute@developer.gserviceaccount.com - string issuer = 2; - - // URL of the provider's public key set to validate signature of the JWT. See - // [OpenID - // Discovery](https://openid.net/specs/openid-connect-discovery-1_0.html#ProviderMetadata). - // Optional if the key set document: - // - can be retrieved from - // [OpenID - // Discovery](https://openid.net/specs/openid-connect-discovery-1_0.html of - // the issuer. - // - can be inferred from the email domain of the issuer (e.g. a Google - // service account). - // - // Example: https://www.googleapis.com/oauth2/v1/certs - string jwks_uri = 3; - - // The list of JWT - // [audiences](https://tools.ietf.org/html/draft-ietf-oauth-json-web-token-32#section-4.1.3). - // that are allowed to access. A JWT containing any of these audiences will - // be accepted. When this setting is absent, only JWTs with audience - // "https://[Service_name][google.api.Service.name]/[API_name][google.protobuf.Api.name]" - // will be accepted. For example, if no audiences are in the setting, - // LibraryService API will only accept JWTs with the following audience - // "https://library-example.googleapis.com/google.example.library.v1.LibraryService". - // - // Example: - // - // audiences: bookstore_android.apps.googleusercontent.com, - // bookstore_web.apps.googleusercontent.com - string audiences = 4; - - // Redirect URL if JWT token is required but not present or is expired. - // Implement authorizationUrl of securityDefinitions in OpenAPI spec. - string authorization_url = 5; -} - -// OAuth scopes are a way to define data and permissions on data. For example, -// there are scopes defined for "Read-only access to Google Calendar" and -// "Access to Cloud Platform". Users can consent to a scope for an application, -// giving it permission to access that data on their behalf. -// -// OAuth scope specifications should be fairly coarse grained; a user will need -// to see and understand the text description of what your scope means. -// -// In most cases: use one or at most two OAuth scopes for an entire family of -// products. If your product has multiple APIs, you should probably be sharing -// the OAuth scope across all of those APIs. -// -// When you need finer grained OAuth consent screens: talk with your product -// management about how developers will use them in practice. -// -// Please note that even though each of the canonical scopes is enough for a -// request to be accepted and passed to the backend, a request can still fail -// due to the backend requiring additional scopes or permissions. -message OAuthRequirements { - // The list of publicly documented OAuth scopes that are allowed access. An - // OAuth token containing any of these scopes will be accepted. - // - // Example: - // - // canonical_scopes: https://www.googleapis.com/auth/calendar, - // https://www.googleapis.com/auth/calendar.read - string canonical_scopes = 1; -} - -// User-defined authentication requirements, including support for -// [JSON Web Token -// (JWT)](https://tools.ietf.org/html/draft-ietf-oauth-json-web-token-32). -message AuthRequirement { - // [id][google.api.AuthProvider.id] from authentication provider. - // - // Example: - // - // provider_id: bookstore_auth - string provider_id = 1; - - // NOTE: This will be deprecated soon, once AuthProvider.audiences is - // implemented and accepted in all the runtime components. - // - // The list of JWT - // [audiences](https://tools.ietf.org/html/draft-ietf-oauth-json-web-token-32#section-4.1.3). - // that are allowed to access. A JWT containing any of these audiences will - // be accepted. When this setting is absent, only JWTs with audience - // "https://[Service_name][google.api.Service.name]/[API_name][google.protobuf.Api.name]" - // will be accepted. For example, if no audiences are in the setting, - // LibraryService API will only accept JWTs with the following audience - // "https://library-example.googleapis.com/google.example.library.v1.LibraryService". - // - // Example: - // - // audiences: bookstore_android.apps.googleusercontent.com, - // bookstore_web.apps.googleusercontent.com - string audiences = 2; -} diff --git a/third_party/google/api/backend.proto b/third_party/google/api/backend.proto deleted file mode 100644 index 26a16ef41..000000000 --- a/third_party/google/api/backend.proto +++ /dev/null @@ -1,127 +0,0 @@ -// Copyright 2019 Google LLC. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -// - -syntax = "proto3"; - -package google.api; - -option go_package = "google.golang.org/genproto/googleapis/api/serviceconfig;serviceconfig"; -option java_multiple_files = true; -option java_outer_classname = "BackendProto"; -option java_package = "com.google.api"; -option objc_class_prefix = "GAPI"; - -// `Backend` defines the backend configuration for a service. -message Backend { - // A list of API backend rules that apply to individual API methods. - // - // **NOTE:** All service configuration rules follow "last one wins" order. - repeated BackendRule rules = 1; -} - -// A backend rule provides configuration for an individual API element. -message BackendRule { - // Path Translation specifies how to combine the backend address with the - // request path in order to produce the appropriate forwarding URL for the - // request. - // - // Path Translation is applicable only to HTTP-based backends. Backends which - // do not accept requests over HTTP/HTTPS should leave `path_translation` - // unspecified. - enum PathTranslation { - PATH_TRANSLATION_UNSPECIFIED = 0; - - // Use the backend address as-is, with no modification to the path. If the - // URL pattern contains variables, the variable names and values will be - // appended to the query string. If a query string parameter and a URL - // pattern variable have the same name, this may result in duplicate keys in - // the query string. - // - // # Examples - // - // Given the following operation config: - // - // Method path: /api/company/{cid}/user/{uid} - // Backend address: https://example.cloudfunctions.net/getUser - // - // Requests to the following request paths will call the backend at the - // translated path: - // - // Request path: /api/company/widgetworks/user/johndoe - // Translated: - // https://example.cloudfunctions.net/getUser?cid=widgetworks&uid=johndoe - // - // Request path: /api/company/widgetworks/user/johndoe?timezone=EST - // Translated: - // https://example.cloudfunctions.net/getUser?timezone=EST&cid=widgetworks&uid=johndoe - CONSTANT_ADDRESS = 1; - - // The request path will be appended to the backend address. - // - // # Examples - // - // Given the following operation config: - // - // Method path: /api/company/{cid}/user/{uid} - // Backend address: https://example.appspot.com - // - // Requests to the following request paths will call the backend at the - // translated path: - // - // Request path: /api/company/widgetworks/user/johndoe - // Translated: - // https://example.appspot.com/api/company/widgetworks/user/johndoe - // - // Request path: /api/company/widgetworks/user/johndoe?timezone=EST - // Translated: - // https://example.appspot.com/api/company/widgetworks/user/johndoe?timezone=EST - APPEND_PATH_TO_ADDRESS = 2; - } - - // Selects the methods to which this rule applies. - // - // Refer to [selector][google.api.DocumentationRule.selector] for syntax details. - string selector = 1; - - // The address of the API backend. - string address = 2; - - // The number of seconds to wait for a response from a request. The default - // deadline for gRPC is infinite (no deadline) and HTTP requests is 5 seconds. - double deadline = 3; - - // Minimum deadline in seconds needed for this method. Calls having deadline - // value lower than this will be rejected. - double min_deadline = 4; - - // The number of seconds to wait for the completion of a long running - // operation. The default is no deadline. - double operation_deadline = 5; - - PathTranslation path_translation = 6; - - // Authentication settings used by the backend. - // - // These are typically used to provide service management functionality to - // a backend served on a publicly-routable URL. The `authentication` - // details should match the authentication behavior used by the backend. - // - // For example, specifying `jwt_audience` implies that the backend expects - // authentication via a JWT. - oneof authentication { - // The JWT audience is used when generating a JWT id token for the backend. - string jwt_audience = 7; - } -} diff --git a/third_party/google/api/billing.proto b/third_party/google/api/billing.proto deleted file mode 100644 index 87c11e7ff..000000000 --- a/third_party/google/api/billing.proto +++ /dev/null @@ -1,67 +0,0 @@ -// Copyright 2019 Google LLC. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -// - -syntax = "proto3"; - -package google.api; - -import "google/api/metric.proto"; - -option go_package = "google.golang.org/genproto/googleapis/api/serviceconfig;serviceconfig"; -option java_multiple_files = true; -option java_outer_classname = "BillingProto"; -option java_package = "com.google.api"; -option objc_class_prefix = "GAPI"; - -// Billing related configuration of the service. -// -// The following example shows how to configure monitored resources and metrics -// for billing: -// -// monitored_resources: -// - type: library.googleapis.com/branch -// labels: -// - key: /city -// description: The city where the library branch is located in. -// - key: /name -// description: The name of the branch. -// metrics: -// - name: library.googleapis.com/book/borrowed_count -// metric_kind: DELTA -// value_type: INT64 -// billing: -// consumer_destinations: -// - monitored_resource: library.googleapis.com/branch -// metrics: -// - library.googleapis.com/book/borrowed_count -message Billing { - // Configuration of a specific billing destination (Currently only support - // bill against consumer project). - message BillingDestination { - // The monitored resource type. The type must be defined in - // [Service.monitored_resources][google.api.Service.monitored_resources] section. - string monitored_resource = 1; - - // Names of the metrics to report to this billing destination. - // Each name must be defined in [Service.metrics][google.api.Service.metrics] section. - repeated string metrics = 2; - } - - // Billing configurations for sending metrics to the consumer project. - // There can be multiple consumer destinations per service, each one must have - // a different monitored resource type. A metric can be used in at most - // one consumer destination. - repeated BillingDestination consumer_destinations = 8; -} diff --git a/third_party/google/api/client.proto b/third_party/google/api/client.proto deleted file mode 100644 index 56f8664aa..000000000 --- a/third_party/google/api/client.proto +++ /dev/null @@ -1,100 +0,0 @@ -// Copyright 2019 Google LLC. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -// - -syntax = "proto3"; - -package google.api; - -import "google/protobuf/descriptor.proto"; - -option go_package = "google.golang.org/genproto/googleapis/api/annotations;annotations"; -option java_multiple_files = true; -option java_outer_classname = "ClientProto"; -option java_package = "com.google.api"; -option objc_class_prefix = "GAPI"; - -extend google.protobuf.MethodOptions { - // A definition of a client library method signature. - // - // In client libraries, each proto RPC corresponds to one or more methods - // which the end user is able to call, and calls the underlying RPC. - // Normally, this method receives a single argument (a struct or instance - // corresponding to the RPC request object). Defining this field will - // add one or more overloads providing flattened or simpler method signatures - // in some languages. - // - // The fields on the method signature are provided as a comma-separated - // string. - // - // For example, the proto RPC and annotation: - // - // rpc CreateSubscription(CreateSubscriptionRequest) - // returns (Subscription) { - // option (google.api.method_signature) = "name,topic"; - // } - // - // Would add the following Java overload (in addition to the method accepting - // the request object): - // - // public final Subscription createSubscription(String name, String topic) - // - // The following backwards-compatibility guidelines apply: - // - // * Adding this annotation to an unannotated method is backwards - // compatible. - // * Adding this annotation to a method which already has existing - // method signature annotations is backwards compatible if and only if - // the new method signature annotation is last in the sequence. - // * Modifying or removing an existing method signature annotation is - // a breaking change. - // * Re-ordering existing method signature annotations is a breaking - // change. - repeated string method_signature = 1051; -} - -extend google.protobuf.ServiceOptions { - // The hostname for this service. - // This should be specified with no prefix or protocol. - // - // Example: - // - // service Foo { - // option (google.api.default_host) = "foo.googleapi.com"; - // ... - // } - string default_host = 1049; - - // OAuth scopes needed for the client. - // - // Example: - // - // service Foo { - // option (google.api.oauth_scopes) = \ - // "https://www.googleapis.com/auth/cloud-platform"; - // ... - // } - // - // If there is more than one scope, use a comma-separated string: - // - // Example: - // - // service Foo { - // option (google.api.oauth_scopes) = \ - // "https://www.googleapis.com/auth/cloud-platform," - // "https://www.googleapis.com/auth/monitoring"; - // ... - // } - string oauth_scopes = 1050; -} diff --git a/third_party/google/api/config_change.proto b/third_party/google/api/config_change.proto deleted file mode 100644 index c36764a5a..000000000 --- a/third_party/google/api/config_change.proto +++ /dev/null @@ -1,85 +0,0 @@ -// Copyright 2019 Google LLC. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -// - -syntax = "proto3"; - -package google.api; - -option go_package = "google.golang.org/genproto/googleapis/api/configchange;configchange"; -option java_multiple_files = true; -option java_outer_classname = "ConfigChangeProto"; -option java_package = "com.google.api"; -option objc_class_prefix = "GAPI"; - -// Output generated from semantically comparing two versions of a service -// configuration. -// -// Includes detailed information about a field that have changed with -// applicable advice about potential consequences for the change, such as -// backwards-incompatibility. -message ConfigChange { - // Object hierarchy path to the change, with levels separated by a '.' - // character. For repeated fields, an applicable unique identifier field is - // used for the index (usually selector, name, or id). For maps, the term - // 'key' is used. If the field has no unique identifier, the numeric index - // is used. - // Examples: - // - visibility.rules[selector=="google.LibraryService.ListBooks"].restriction - // - quota.metric_rules[selector=="google"].metric_costs[key=="reads"].value - // - logging.producer_destinations[0] - string element = 1; - - // Value of the changed object in the old Service configuration, - // in JSON format. This field will not be populated if ChangeType == ADDED. - string old_value = 2; - - // Value of the changed object in the new Service configuration, - // in JSON format. This field will not be populated if ChangeType == REMOVED. - string new_value = 3; - - // The type for this change, either ADDED, REMOVED, or MODIFIED. - ChangeType change_type = 4; - - // Collection of advice provided for this change, useful for determining the - // possible impact of this change. - repeated Advice advices = 5; -} - -// Generated advice about this change, used for providing more -// information about how a change will affect the existing service. -message Advice { - // Useful description for why this advice was applied and what actions should - // be taken to mitigate any implied risks. - string description = 2; -} - -// Classifies set of possible modifications to an object in the service -// configuration. -enum ChangeType { - // No value was provided. - CHANGE_TYPE_UNSPECIFIED = 0; - - // The changed object exists in the 'new' service configuration, but not - // in the 'old' service configuration. - ADDED = 1; - - // The changed object exists in the 'old' service configuration, but not - // in the 'new' service configuration. - REMOVED = 2; - - // The changed object exists in both service configurations, but its value - // is different. - MODIFIED = 3; -} diff --git a/third_party/google/api/consumer.proto b/third_party/google/api/consumer.proto deleted file mode 100644 index 0facc2eb1..000000000 --- a/third_party/google/api/consumer.proto +++ /dev/null @@ -1,82 +0,0 @@ -// Copyright 2016 Google Inc. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -syntax = "proto3"; - -package google.api; - -option go_package = "google.golang.org/genproto/googleapis/api/serviceconfig;serviceconfig"; -option java_multiple_files = true; -option java_outer_classname = "ConsumerProto"; -option java_package = "com.google.api"; - -// A descriptor for defining project properties for a service. One service may -// have many consumer projects, and the service may want to behave differently -// depending on some properties on the project. For example, a project may be -// associated with a school, or a business, or a government agency, a business -// type property on the project may affect how a service responds to the client. -// This descriptor defines which properties are allowed to be set on a project. -// -// Example: -// -// project_properties: -// properties: -// - name: NO_WATERMARK -// type: BOOL -// description: Allows usage of the API without watermarks. -// - name: EXTENDED_TILE_CACHE_PERIOD -// type: INT64 -message ProjectProperties { - // List of per consumer project-specific properties. - repeated Property properties = 1; -} - -// Defines project properties. -// -// API services can define properties that can be assigned to consumer projects -// so that backends can perform response customization without having to make -// additional calls or maintain additional storage. For example, Maps API -// defines properties that controls map tile cache period, or whether to embed a -// watermark in a result. -// -// These values can be set via API producer console. Only API providers can -// define and set these properties. -message Property { - // Supported data type of the property values - enum PropertyType { - // The type is unspecified, and will result in an error. - UNSPECIFIED = 0; - - // The type is `int64`. - INT64 = 1; - - // The type is `bool`. - BOOL = 2; - - // The type is `string`. - STRING = 3; - - // The type is 'double'. - DOUBLE = 4; - } - - // The name of the property (a.k.a key). - string name = 1; - - // The type of this property. - PropertyType type = 2; - - // The description of the property - string description = 3; -} diff --git a/third_party/google/api/context.proto b/third_party/google/api/context.proto deleted file mode 100644 index 2d330f6f2..000000000 --- a/third_party/google/api/context.proto +++ /dev/null @@ -1,90 +0,0 @@ -// Copyright 2019 Google LLC. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -// - -syntax = "proto3"; - -package google.api; - -option go_package = "google.golang.org/genproto/googleapis/api/serviceconfig;serviceconfig"; -option java_multiple_files = true; -option java_outer_classname = "ContextProto"; -option java_package = "com.google.api"; -option objc_class_prefix = "GAPI"; - -// `Context` defines which contexts an API requests. -// -// Example: -// -// context: -// rules: -// - selector: "*" -// requested: -// - google.rpc.context.ProjectContext -// - google.rpc.context.OriginContext -// -// The above specifies that all methods in the API request -// `google.rpc.context.ProjectContext` and -// `google.rpc.context.OriginContext`. -// -// Available context types are defined in package -// `google.rpc.context`. -// -// This also provides mechanism to whitelist any protobuf message extension that -// can be sent in grpc metadata using “x-goog-ext--bin” and -// “x-goog-ext--jspb” format. For example, list any service -// specific protobuf types that can appear in grpc metadata as follows in your -// yaml file: -// -// Example: -// -// context: -// rules: -// - selector: "google.example.library.v1.LibraryService.CreateBook" -// allowed_request_extensions: -// - google.foo.v1.NewExtension -// allowed_response_extensions: -// - google.foo.v1.NewExtension -// -// You can also specify extension ID instead of fully qualified extension name -// here. -message Context { - // A list of RPC context rules that apply to individual API methods. - // - // **NOTE:** All service configuration rules follow "last one wins" order. - repeated ContextRule rules = 1; -} - -// A context rule provides information about the context for an individual API -// element. -message ContextRule { - // Selects the methods to which this rule applies. - // - // Refer to [selector][google.api.DocumentationRule.selector] for syntax details. - string selector = 1; - - // A list of full type names of requested contexts. - repeated string requested = 2; - - // A list of full type names of provided contexts. - repeated string provided = 3; - - // A list of full type names or extension IDs of extensions allowed in grpc - // side channel from client to backend. - repeated string allowed_request_extensions = 4; - - // A list of full type names or extension IDs of extensions allowed in grpc - // side channel from backend to client. - repeated string allowed_response_extensions = 5; -} diff --git a/third_party/google/api/control.proto b/third_party/google/api/control.proto deleted file mode 100644 index e769f9783..000000000 --- a/third_party/google/api/control.proto +++ /dev/null @@ -1,33 +0,0 @@ -// Copyright 2019 Google LLC. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -// - -syntax = "proto3"; - -package google.api; - -option go_package = "google.golang.org/genproto/googleapis/api/serviceconfig;serviceconfig"; -option java_multiple_files = true; -option java_outer_classname = "ControlProto"; -option java_package = "com.google.api"; -option objc_class_prefix = "GAPI"; - -// Selects and configures the service controller used by the service. The -// service controller handles features like abuse, quota, billing, logging, -// monitoring, etc. -message Control { - // The service control environment to use. If empty, no control plane - // feature (like quota and billing) will be enabled. - string environment = 1; -} diff --git a/third_party/google/api/distribution.proto b/third_party/google/api/distribution.proto deleted file mode 100644 index ee45d9dcd..000000000 --- a/third_party/google/api/distribution.proto +++ /dev/null @@ -1,212 +0,0 @@ -// Copyright 2019 Google LLC. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -// - -syntax = "proto3"; - -package google.api; - -import "google/protobuf/any.proto"; -import "google/protobuf/timestamp.proto"; - -option go_package = "google.golang.org/genproto/googleapis/api/distribution;distribution"; -option java_multiple_files = true; -option java_outer_classname = "DistributionProto"; -option java_package = "com.google.api"; -option objc_class_prefix = "GAPI"; - -// `Distribution` contains summary statistics for a population of values. It -// optionally contains a histogram representing the distribution of those values -// across a set of buckets. -// -// The summary statistics are the count, mean, sum of the squared deviation from -// the mean, the minimum, and the maximum of the set of population of values. -// The histogram is based on a sequence of buckets and gives a count of values -// that fall into each bucket. The boundaries of the buckets are given either -// explicitly or by formulas for buckets of fixed or exponentially increasing -// widths. -// -// Although it is not forbidden, it is generally a bad idea to include -// non-finite values (infinities or NaNs) in the population of values, as this -// will render the `mean` and `sum_of_squared_deviation` fields meaningless. -message Distribution { - // The range of the population values. - message Range { - // The minimum of the population values. - double min = 1; - - // The maximum of the population values. - double max = 2; - } - - // `BucketOptions` describes the bucket boundaries used to create a histogram - // for the distribution. The buckets can be in a linear sequence, an - // exponential sequence, or each bucket can be specified explicitly. - // `BucketOptions` does not include the number of values in each bucket. - // - // A bucket has an inclusive lower bound and exclusive upper bound for the - // values that are counted for that bucket. The upper bound of a bucket must - // be strictly greater than the lower bound. The sequence of N buckets for a - // distribution consists of an underflow bucket (number 0), zero or more - // finite buckets (number 1 through N - 2) and an overflow bucket (number N - - // 1). The buckets are contiguous: the lower bound of bucket i (i > 0) is the - // same as the upper bound of bucket i - 1. The buckets span the whole range - // of finite values: lower bound of the underflow bucket is -infinity and the - // upper bound of the overflow bucket is +infinity. The finite buckets are - // so-called because both bounds are finite. - message BucketOptions { - // Specifies a linear sequence of buckets that all have the same width - // (except overflow and underflow). Each bucket represents a constant - // absolute uncertainty on the specific value in the bucket. - // - // There are `num_finite_buckets + 2` (= N) buckets. Bucket `i` has the - // following boundaries: - // - // Upper bound (0 <= i < N-1): offset + (width * i). - // Lower bound (1 <= i < N): offset + (width * (i - 1)). - message Linear { - // Must be greater than 0. - int32 num_finite_buckets = 1; - - // Must be greater than 0. - double width = 2; - - // Lower bound of the first bucket. - double offset = 3; - } - - // Specifies an exponential sequence of buckets that have a width that is - // proportional to the value of the lower bound. Each bucket represents a - // constant relative uncertainty on a specific value in the bucket. - // - // There are `num_finite_buckets + 2` (= N) buckets. Bucket `i` has the - // following boundaries: - // - // Upper bound (0 <= i < N-1): scale * (growth_factor ^ i). - // Lower bound (1 <= i < N): scale * (growth_factor ^ (i - 1)). - message Exponential { - // Must be greater than 0. - int32 num_finite_buckets = 1; - - // Must be greater than 1. - double growth_factor = 2; - - // Must be greater than 0. - double scale = 3; - } - - // Specifies a set of buckets with arbitrary widths. - // - // There are `size(bounds) + 1` (= N) buckets. Bucket `i` has the following - // boundaries: - // - // Upper bound (0 <= i < N-1): bounds[i] - // Lower bound (1 <= i < N); bounds[i - 1] - // - // The `bounds` field must contain at least one element. If `bounds` has - // only one element, then there are no finite buckets, and that single - // element is the common boundary of the overflow and underflow buckets. - message Explicit { - // The values must be monotonically increasing. - repeated double bounds = 1; - } - - // Exactly one of these three fields must be set. - oneof options { - // The linear bucket. - Linear linear_buckets = 1; - - // The exponential buckets. - Exponential exponential_buckets = 2; - - // The explicit buckets. - Explicit explicit_buckets = 3; - } - } - - // Exemplars are example points that may be used to annotate aggregated - // distribution values. They are metadata that gives information about a - // particular value added to a Distribution bucket, such as a trace ID that - // was active when a value was added. They may contain further information, - // such as a example values and timestamps, origin, etc. - message Exemplar { - // Value of the exemplar point. This value determines to which bucket the - // exemplar belongs. - double value = 1; - - // The observation (sampling) time of the above value. - google.protobuf.Timestamp timestamp = 2; - - // Contextual information about the example value. Examples are: - // - // Trace: type.googleapis.com/google.monitoring.v3.SpanContext - // - // Literal string: type.googleapis.com/google.protobuf.StringValue - // - // Labels dropped during aggregation: - // type.googleapis.com/google.monitoring.v3.DroppedLabels - // - // There may be only a single attachment of any given message type in a - // single exemplar, and this is enforced by the system. - repeated google.protobuf.Any attachments = 3; - } - - // The number of values in the population. Must be non-negative. This value - // must equal the sum of the values in `bucket_counts` if a histogram is - // provided. - int64 count = 1; - - // The arithmetic mean of the values in the population. If `count` is zero - // then this field must be zero. - double mean = 2; - - // The sum of squared deviations from the mean of the values in the - // population. For values x_i this is: - // - // Sum[i=1..n]((x_i - mean)^2) - // - // Knuth, "The Art of Computer Programming", Vol. 2, page 323, 3rd edition - // describes Welford's method for accumulating this sum in one pass. - // - // If `count` is zero then this field must be zero. - double sum_of_squared_deviation = 3; - - // If specified, contains the range of the population values. The field - // must not be present if the `count` is zero. - Range range = 4; - - // Defines the histogram bucket boundaries. If the distribution does not - // contain a histogram, then omit this field. - BucketOptions bucket_options = 6; - - // The number of values in each bucket of the histogram, as described in - // `bucket_options`. If the distribution does not have a histogram, then omit - // this field. If there is a histogram, then the sum of the values in - // `bucket_counts` must equal the value in the `count` field of the - // distribution. - // - // If present, `bucket_counts` should contain N values, where N is the number - // of buckets specified in `bucket_options`. If you supply fewer than N - // values, the remaining values are assumed to be 0. - // - // The order of the values in `bucket_counts` follows the bucket numbering - // schemes described for the three bucket types. The first value must be the - // count for the underflow bucket (number 0). The next N-2 values are the - // counts for the finite buckets (number 1 through N-2). The N'th value in - // `bucket_counts` is the count for the overflow bucket (number N-1). - repeated int64 bucket_counts = 7; - - // Must be in increasing order of `value` field. - repeated Exemplar exemplars = 10; -} diff --git a/third_party/google/api/documentation.proto b/third_party/google/api/documentation.proto deleted file mode 100644 index 74cbf67e9..000000000 --- a/third_party/google/api/documentation.proto +++ /dev/null @@ -1,157 +0,0 @@ -// Copyright 2019 Google LLC. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -// - -syntax = "proto3"; - -package google.api; - -option go_package = "google.golang.org/genproto/googleapis/api/serviceconfig;serviceconfig"; -option java_multiple_files = true; -option java_outer_classname = "DocumentationProto"; -option java_package = "com.google.api"; -option objc_class_prefix = "GAPI"; - -// `Documentation` provides the information for describing a service. -// -// Example: -//
documentation:
-//   summary: >
-//     The Google Calendar API gives access
-//     to most calendar features.
-//   pages:
-//   - name: Overview
-//     content: (== include google/foo/overview.md ==)
-//   - name: Tutorial
-//     content: (== include google/foo/tutorial.md ==)
-//     subpages;
-//     - name: Java
-//       content: (== include google/foo/tutorial_java.md ==)
-//   rules:
-//   - selector: google.calendar.Calendar.Get
-//     description: >
-//       ...
-//   - selector: google.calendar.Calendar.Put
-//     description: >
-//       ...
-// 
-// Documentation is provided in markdown syntax. In addition to -// standard markdown features, definition lists, tables and fenced -// code blocks are supported. Section headers can be provided and are -// interpreted relative to the section nesting of the context where -// a documentation fragment is embedded. -// -// Documentation from the IDL is merged with documentation defined -// via the config at normalization time, where documentation provided -// by config rules overrides IDL provided. -// -// A number of constructs specific to the API platform are supported -// in documentation text. -// -// In order to reference a proto element, the following -// notation can be used: -//
[fully.qualified.proto.name][]
-// To override the display text used for the link, this can be used: -//
[display text][fully.qualified.proto.name]
-// Text can be excluded from doc using the following notation: -//
(-- internal comment --)
-// -// A few directives are available in documentation. Note that -// directives must appear on a single line to be properly -// identified. The `include` directive includes a markdown file from -// an external source: -//
(== include path/to/file ==)
-// The `resource_for` directive marks a message to be the resource of -// a collection in REST view. If it is not specified, tools attempt -// to infer the resource from the operations in a collection: -//
(== resource_for v1.shelves.books ==)
-// The directive `suppress_warning` does not directly affect documentation -// and is documented together with service config validation. -message Documentation { - // A short summary of what the service does. Can only be provided by - // plain text. - string summary = 1; - - // The top level pages for the documentation set. - repeated Page pages = 5; - - // A list of documentation rules that apply to individual API elements. - // - // **NOTE:** All service configuration rules follow "last one wins" order. - repeated DocumentationRule rules = 3; - - // The URL to the root of documentation. - string documentation_root_url = 4; - - // Declares a single overview page. For example: - //
documentation:
-  //   summary: ...
-  //   overview: (== include overview.md ==)
-  // 
- // This is a shortcut for the following declaration (using pages style): - //
documentation:
-  //   summary: ...
-  //   pages:
-  //   - name: Overview
-  //     content: (== include overview.md ==)
-  // 
- // Note: you cannot specify both `overview` field and `pages` field. - string overview = 2; -} - -// A documentation rule provides information about individual API elements. -message DocumentationRule { - // The selector is a comma-separated list of patterns. Each pattern is a - // qualified name of the element which may end in "*", indicating a wildcard. - // Wildcards are only allowed at the end and for a whole component of the - // qualified name, i.e. "foo.*" is ok, but not "foo.b*" or "foo.*.bar". A - // wildcard will match one or more components. To specify a default for all - // applicable elements, the whole pattern "*" is used. - string selector = 1; - - // Description of the selected API(s). - string description = 2; - - // Deprecation description of the selected element(s). It can be provided if - // an element is marked as `deprecated`. - string deprecation_description = 3; -} - -// Represents a documentation page. A page can contain subpages to represent -// nested documentation set structure. -message Page { - // The name of the page. It will be used as an identity of the page to - // generate URI of the page, text of the link to this page in navigation, - // etc. The full page name (start from the root page name to this page - // concatenated with `.`) can be used as reference to the page in your - // documentation. For example: - //
pages:
-  // - name: Tutorial
-  //   content: (== include tutorial.md ==)
-  //   subpages:
-  //   - name: Java
-  //     content: (== include tutorial_java.md ==)
-  // 
- // You can reference `Java` page using Markdown reference link syntax: - // `[Java][Tutorial.Java]`. - string name = 1; - - // The Markdown content of the page. You can use (== include {path} - // ==) to include content from a Markdown file. - string content = 2; - - // Subpages of this page. The order of subpages specified here will be - // honored in the generated docset. - repeated Page subpages = 3; -} diff --git a/third_party/google/api/endpoint.proto b/third_party/google/api/endpoint.proto deleted file mode 100644 index 2077334d2..000000000 --- a/third_party/google/api/endpoint.proto +++ /dev/null @@ -1,70 +0,0 @@ -// Copyright 2019 Google LLC. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -// - -syntax = "proto3"; - -package google.api; - -option go_package = "google.golang.org/genproto/googleapis/api/serviceconfig;serviceconfig"; -option java_multiple_files = true; -option java_outer_classname = "EndpointProto"; -option java_package = "com.google.api"; -option objc_class_prefix = "GAPI"; - -// `Endpoint` describes a network endpoint that serves a set of APIs. -// A service may expose any number of endpoints, and all endpoints share the -// same service configuration, such as quota configuration and monitoring -// configuration. -// -// Example service configuration: -// -// name: library-example.googleapis.com -// endpoints: -// # Below entry makes 'google.example.library.v1.Library' -// # API be served from endpoint address library-example.googleapis.com. -// # It also allows HTTP OPTIONS calls to be passed to the backend, for -// # it to decide whether the subsequent cross-origin request is -// # allowed to proceed. -// - name: library-example.googleapis.com -// allow_cors: true -message Endpoint { - // The canonical name of this endpoint. - string name = 1; - - // DEPRECATED: This field is no longer supported. Instead of using aliases, - // please specify multiple [google.api.Endpoint][google.api.Endpoint] for each of the intended - // aliases. - // - // Additional names that this endpoint will be hosted on. - repeated string aliases = 2 [deprecated = true]; - - // The list of features enabled on this endpoint. - repeated string features = 4; - - // The specification of an Internet routable address of API frontend that will - // handle requests to this [API - // Endpoint](https://cloud.google.com/apis/design/glossary). It should be - // either a valid IPv4 address or a fully-qualified domain name. For example, - // "8.8.8.8" or "myservice.appspot.com". - string target = 101; - - // Allowing - // [CORS](https://en.wikipedia.org/wiki/Cross-origin_resource_sharing), aka - // cross-domain traffic, would allow the backends served from this endpoint to - // receive and respond to HTTP OPTIONS requests. The response will be used by - // the browser to determine whether the subsequent cross-origin request is - // allowed to proceed. - bool allow_cors = 5; -} diff --git a/third_party/google/api/experimental/authorization_config.proto b/third_party/google/api/experimental/authorization_config.proto deleted file mode 100644 index 4fb24ecdb..000000000 --- a/third_party/google/api/experimental/authorization_config.proto +++ /dev/null @@ -1,40 +0,0 @@ -// Copyright 2018 Google LLC. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -// - -syntax = "proto3"; - -package google.api; - -option go_package = "google.golang.org/genproto/googleapis/api;api"; -option java_multiple_files = true; -option java_outer_classname = "AuthorizationConfigProto"; -option java_package = "com.google.api"; -option objc_class_prefix = "GAPI"; - -// Configuration of authorization. -// -// This section determines the authorization provider, if unspecified, then no -// authorization check will be done. -// -// Example: -// -// experimental: -// authorization: -// provider: firebaserules.googleapis.com -message AuthorizationConfig { - // The name of the authorization provider, such as - // firebaserules.googleapis.com. - string provider = 1; -} diff --git a/third_party/google/api/experimental/experimental.proto b/third_party/google/api/experimental/experimental.proto deleted file mode 100644 index 6f40d71f1..000000000 --- a/third_party/google/api/experimental/experimental.proto +++ /dev/null @@ -1,34 +0,0 @@ -// Copyright 2018 Google LLC. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -// - -syntax = "proto3"; - -package google.api; - -import "google/api/annotations.proto"; -import "google/api/experimental/authorization_config.proto"; - -option go_package = "google.golang.org/genproto/googleapis/api;api"; -option java_multiple_files = true; -option java_outer_classname = "ExperimentalProto"; -option java_package = "com.google.api"; -option objc_class_prefix = "GAPI"; - -// Experimental service configuration. These configuration options can -// only be used by whitelisted users. -message Experimental { - // Authorization configuration. - AuthorizationConfig authorization = 8; -} diff --git a/third_party/google/api/expr/artman_cel.yaml b/third_party/google/api/expr/artman_cel.yaml deleted file mode 100644 index 89d2d28b2..000000000 --- a/third_party/google/api/expr/artman_cel.yaml +++ /dev/null @@ -1,37 +0,0 @@ -# This file is auto-generated based on service config and could be -# incorrect. The API producer can manually edit it. Remove all the FIXMEs -# before sending this file out for review. -common: - api_name: cel - api_version: v1alpha1 - organization_name: google-cloud - proto_deps: - - name: google-common-protos - src_proto_paths: - - v1alpha1 - service_yaml: cel.yaml - gapic_yaml: v1alpha1/cel_gapic.yaml -artifacts: -- name: gapic_config - type: GAPIC_CONFIG -- name: java_gapic - type: GAPIC - language: JAVA -- name: python_gapic - type: GAPIC - language: PYTHON -- name: nodejs_gapic - type: GAPIC - language: NODEJS -- name: php_gapic - type: GAPIC - language: PHP -- name: go_gapic - type: GAPIC - language: GO -- name: ruby_gapic - type: GAPIC - language: RUBY -- name: csharp_gapic - type: GAPIC - language: CSHARP diff --git a/third_party/google/api/expr/cel.yaml b/third_party/google/api/expr/cel.yaml deleted file mode 100644 index bbe7fbde1..000000000 --- a/third_party/google/api/expr/cel.yaml +++ /dev/null @@ -1,61 +0,0 @@ -type: google.api.Service -config_version: 3 -name: cel.googleapis.com -title: Common Expression Language - -apis: -- name: google.api.expr.v1alpha1.ConformanceService -- name: google.api.expr.v1alpha1.CelService - -documentation: - summary: Defines common types for the Common Expression Language. - overview: |- - # Common Expression Language - - The Common Expression Language (CEL) implements common semantics for - expression evaluation, enabling different applications to more easily - interoperate. - - Key Applications - - * Security policy: organization have complex infrastructure and need - common tooling to reason about the system as a whole * Protocols: - expressions are a useful data type and require interoperability across - programming languages and platforms. - - - - Guiding philosophy: - - 1. Keep it small & fast. * CEL evaluates in linear time, is mutation - free, and not Turing-complete. This limitation is a feature of the language - design, which allows the implementation to evaluate orders of magnitude - faster than equivalently sandboxed JavaScript. 2. Make it extensible. * - CEL is designed to be embedded in applications, and allows for extensibility - via its context which allows for functions and data to be provided by the - software that embeds it. 3. Developer-friendly * The language is - approachable to developers. The initial spec was based on the experience of - developing Firebase Rules and usability testing many prior iterations. * - The library itself and accompanying toolings should be easy to adopt by - teams that seek to integrate CEL into their platforms. - - The required components of a system that supports CEL are: - - * The textual representation of an expression as written by a developer. - It is of similar syntax of expressions in C/C++/Java/JavaScript * A binary - representation of an expression. It is an abstract syntax tree (AST). * A - compiler library that converts the textual representation to the binary - representation. This can be done ahead of time (in the control plane) or - just before evaluation (in the data plane). * A context containing one or - more typed variables, often protobuf messages. Most use-case will use - attribute_context.proto * An evaluator library that takes the binary - format in the context and produces a result, usually a Boolean. - - Example of boolean conditions and object construction: - - ``` c // Condition account.balance >= transaction.withdrawal || - (account.overdraftProtection && account.overdraftLimit >= - transaction.withdrawal - account.balance) - - // Object construction common.GeoPoint{ latitude: 10.0, longitude: -5.5 } - ``` diff --git a/third_party/google/api/expr/v1alpha1/cel_gapic.yaml b/third_party/google/api/expr/v1alpha1/cel_gapic.yaml deleted file mode 100644 index 68a3fd0a2..000000000 --- a/third_party/google/api/expr/v1alpha1/cel_gapic.yaml +++ /dev/null @@ -1,246 +0,0 @@ -type: com.google.api.codegen.ConfigProto -config_schema_version: 1.0.0 -# The settings of generated code in a specific language. -language_settings: - java: - package_name: com.google.cloud.api.expr.v1alpha1 - python: - package_name: google.cloud.api.expr_v1alpha1.gapic - go: - package_name: cloud.google.com/go/expr/apiv1alpha1 - csharp: - package_name: Google.Api.Expr.V1alpha1 - ruby: - package_name: Google::Cloud::Api::Expr::V1alpha1 - php: - package_name: Google\Cloud\Api\Expr\V1alpha1 - nodejs: - package_name: expr.v1alpha1 -# A list of API interface configurations. -interfaces: - # The fully qualified name of the API interface. -- name: google.api.expr.v1alpha1.CelService - # A list of resource collection configurations. - # Consists of a name_pattern and an entity_name. - # The name_pattern is a pattern to describe the names of the resources of this - # collection, using the platform's conventions for URI patterns. A generator - # may use this to generate methods to compose and decompose such names. The - # pattern should use named placeholders as in `shelves/{shelf}/books/{book}`; - # those will be taken as hints for the parameter names of the generated - # methods. If empty, no name methods are generated. - # The entity_name is the name to be used as a basis for generated methods and - # classes. - collections: [] - # Definition for retryable codes. - retry_codes_def: - - name: idempotent - retry_codes: - - UNAVAILABLE - - name: non_idempotent - retry_codes: [] - # Definition for retry/backoff parameters. - retry_params_def: - - name: default - initial_retry_delay_millis: 100 - retry_delay_multiplier: 1.3 - max_retry_delay_millis: 60000 - initial_rpc_timeout_millis: 20000 - rpc_timeout_multiplier: 1 - max_rpc_timeout_millis: 20000 - total_timeout_millis: 600000 - # A list of method configurations. - # Common properties: - # - # name - The simple name of the method. - # - # flattening - Specifies the configuration for parameter flattening. - # Describes the parameter groups for which a generator should produce method - # overloads which allow a client to directly pass request message fields as - # method parameters. This information may or may not be used, depending on - # the target language. - # Consists of groups, which each represent a list of parameters to be - # flattened. Each parameter listed must be a field of the request message. - # - # required_fields - Fields that are always required for a request to be - # valid. - # - # resource_name_treatment - An enum that specifies how to treat the resource - # name formats defined in the field_name_patterns and - # response_field_name_patterns fields. - # UNSET: default value - # NONE: the collection configs will not be used by the generated code. - # VALIDATE: string fields will be validated by the client against the - # specified resource name formats. - # STATIC_TYPES: the client will use generated types for resource names. - # - # page_streaming - Specifies the configuration for paging. - # Describes information for generating a method which transforms a paging - # list RPC into a stream of resources. - # Consists of a request and a response. - # The request specifies request information of the list method. It defines - # which fields match the paging pattern in the request. The request consists - # of a page_size_field and a token_field. The page_size_field is the name of - # the optional field specifying the maximum number of elements to be - # returned in the response. The token_field is the name of the field in the - # request containing the page token. - # The response specifies response information of the list method. It defines - # which fields match the paging pattern in the response. The response - # consists of a token_field and a resources_field. The token_field is the - # name of the field in the response containing the next page token. The - # resources_field is the name of the field in the response containing the - # list of resources belonging to the page. - # - # retry_codes_name - Specifies the configuration for retryable codes. The - # name must be defined in interfaces.retry_codes_def. - # - # retry_params_name - Specifies the configuration for retry/backoff - # parameters. The name must be defined in interfaces.retry_params_def. - # - # field_name_patterns - Maps the field name of the request type to - # entity_name of interfaces.collections. - # Specifies the string pattern that the field must follow. - # - # timeout_millis - Specifies the default timeout for a non-retrying call. If - # the call is retrying, refer to retry_params_name instead. - methods: - - name: Parse - flattening: - groups: - - parameters: - - cel_source - required_fields: - - cel_source - retry_codes_name: non_idempotent - retry_params_name: default - timeout_millis: 60000 - - name: Check - flattening: - groups: - - parameters: - - parsed_expr - required_fields: - - parsed_expr - retry_codes_name: non_idempotent - retry_params_name: default - timeout_millis: 60000 - - name: Eval - flattening: - groups: - - parameters: - required_fields: - retry_codes_name: non_idempotent - retry_params_name: default - timeout_millis: 60000 - # The fully qualified name of the API interface. -- name: google.api.expr.v1alpha1.ConformanceService - # A list of resource collection configurations. - # Consists of a name_pattern and an entity_name. - # The name_pattern is a pattern to describe the names of the resources of this - # collection, using the platform's conventions for URI patterns. A generator - # may use this to generate methods to compose and decompose such names. The - # pattern should use named placeholders as in `shelves/{shelf}/books/{book}`; - # those will be taken as hints for the parameter names of the generated - # methods. If empty, no name methods are generated. - # The entity_name is the name to be used as a basis for generated methods and - # classes. - collections: [] - # Definition for retryable codes. - retry_codes_def: - - name: idempotent - retry_codes: - - UNAVAILABLE - - name: non_idempotent - retry_codes: [] - # Definition for retry/backoff parameters. - retry_params_def: - - name: default - initial_retry_delay_millis: 100 - retry_delay_multiplier: 1.3 - max_retry_delay_millis: 60000 - initial_rpc_timeout_millis: 20000 - rpc_timeout_multiplier: 1 - max_rpc_timeout_millis: 20000 - total_timeout_millis: 600000 - # A list of method configurations. - # Common properties: - # - # name - The simple name of the method. - # - # flattening - Specifies the configuration for parameter flattening. - # Describes the parameter groups for which a generator should produce method - # overloads which allow a client to directly pass request message fields as - # method parameters. This information may or may not be used, depending on - # the target language. - # Consists of groups, which each represent a list of parameters to be - # flattened. Each parameter listed must be a field of the request message. - # - # required_fields - Fields that are always required for a request to be - # valid. - # - # resource_name_treatment - An enum that specifies how to treat the resource - # name formats defined in the field_name_patterns and - # response_field_name_patterns fields. - # UNSET: default value - # NONE: the collection configs will not be used by the generated code. - # VALIDATE: string fields will be validated by the client against the - # specified resource name formats. - # STATIC_TYPES: the client will use generated types for resource names. - # - # page_streaming - Specifies the configuration for paging. - # Describes information for generating a method which transforms a paging - # list RPC into a stream of resources. - # Consists of a request and a response. - # The request specifies request information of the list method. It defines - # which fields match the paging pattern in the request. The request consists - # of a page_size_field and a token_field. The page_size_field is the name of - # the optional field specifying the maximum number of elements to be - # returned in the response. The token_field is the name of the field in the - # request containing the page token. - # The response specifies response information of the list method. It defines - # which fields match the paging pattern in the response. The response - # consists of a token_field and a resources_field. The token_field is the - # name of the field in the response containing the next page token. The - # resources_field is the name of the field in the response containing the - # list of resources belonging to the page. - # - # retry_codes_name - Specifies the configuration for retryable codes. The - # name must be defined in interfaces.retry_codes_def. - # - # retry_params_name - Specifies the configuration for retry/backoff - # parameters. The name must be defined in interfaces.retry_params_def. - # - # field_name_patterns - Maps the field name of the request type to - # entity_name of interfaces.collections. - # Specifies the string pattern that the field must follow. - # - # timeout_millis - Specifies the default timeout for a non-retrying call. If - # the call is retrying, refer to retry_params_name instead. - methods: - - name: Parse - flattening: - groups: - - parameters: - - cel_source - required_fields: - - cel_source - retry_codes_name: non_idempotent - retry_params_name: default - timeout_millis: 60000 - - name: Check - flattening: - groups: - - parameters: - - parsed_expr - required_fields: - - parsed_expr - retry_codes_name: non_idempotent - retry_params_name: default - timeout_millis: 60000 - - name: Eval - flattening: - groups: - - parameters: - required_fields: - retry_codes_name: non_idempotent - retry_params_name: default - timeout_millis: 60000 diff --git a/third_party/google/api/expr/v1alpha1/cel_service.proto b/third_party/google/api/expr/v1alpha1/cel_service.proto deleted file mode 100644 index 0bf649ff5..000000000 --- a/third_party/google/api/expr/v1alpha1/cel_service.proto +++ /dev/null @@ -1,44 +0,0 @@ -// Copyright 2018 Google LLC. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -// - -syntax = "proto3"; - -package google.api.expr.v1alpha1; - -import "google/api/expr/v1alpha1/conformance_service.proto"; - -option cc_enable_arenas = true; -option go_package = "google.golang.org/genproto/googleapis/api/expr/v1alpha1;expr"; -option java_multiple_files = true; -option java_outer_classname = "CelServiceProto"; -option java_package = "com.google.api.expr.v1alpha1"; - -// Access a CEL implementation from another process or machine. -// A CEL implementation is decomposed as a parser, a static checker, -// and an evaluator. Every CEL implementation is expected to provide -// a server for this API. The API will be used for conformance testing, -// utilities, and execution as a service. -service CelService { - // Transforms CEL source text into a parsed representation. - rpc Parse(ParseRequest) returns (ParseResponse) {} - - // Runs static checks on a parsed CEL representation and return - // an annotated representation, or a set of issues. - rpc Check(CheckRequest) returns (CheckResponse) {} - - // Evaluates a parsed or annotation CEL representation given - // values of external bindings. - rpc Eval(EvalRequest) returns (EvalResponse) {} -} diff --git a/third_party/google/api/expr/v1alpha1/checked.proto b/third_party/google/api/expr/v1alpha1/checked.proto deleted file mode 100644 index 60dd09e20..000000000 --- a/third_party/google/api/expr/v1alpha1/checked.proto +++ /dev/null @@ -1,336 +0,0 @@ -// Copyright 2018 Google LLC. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -// - -syntax = "proto3"; - -package google.api.expr.v1alpha1; - -import "google/api/expr/v1alpha1/syntax.proto"; -import "google/protobuf/empty.proto"; -import "google/protobuf/struct.proto"; - -option cc_enable_arenas = true; -option go_package = "google.golang.org/genproto/googleapis/api/expr/v1alpha1;expr"; -option java_multiple_files = true; -option java_outer_classname = "DeclProto"; -option java_package = "com.google.api.expr.v1alpha1"; - -// Protos for representing CEL declarations and typed checked expressions. - -// A CEL expression which has been successfully type checked. -message CheckedExpr { - // A map from expression ids to resolved references. - // - // The following entries are in this table: - // - // - An Ident or Select expression is represented here if it resolves to a - // declaration. For instance, if `a.b.c` is represented by - // `select(select(id(a), b), c)`, and `a.b` resolves to a declaration, - // while `c` is a field selection, then the reference is attached to the - // nested select expression (but not to the id or or the outer select). - // In turn, if `a` resolves to a declaration and `b.c` are field selections, - // the reference is attached to the ident expression. - // - Every Call expression has an entry here, identifying the function being - // called. - // - Every CreateStruct expression for a message has an entry, identifying - // the message. - map reference_map = 2; - - // A map from expression ids to types. - // - // Every expression node which has a type different than DYN has a mapping - // here. If an expression has type DYN, it is omitted from this map to save - // space. - map type_map = 3; - - // The source info derived from input that generated the parsed `expr` and - // any optimizations made during the type-checking pass. - SourceInfo source_info = 5; - - // The checked expression. Semantically equivalent to the parsed `expr`, but - // may have structural differences. - Expr expr = 4; -} - -// Represents a CEL type. -message Type { - // List type with typed elements, e.g. `list`. - message ListType { - // The element type. - Type elem_type = 1; - } - - // Map type with parameterized key and value types, e.g. `map`. - message MapType { - // The type of the key. - Type key_type = 1; - - // The type of the value. - Type value_type = 2; - } - - // Function type with result and arg types. - message FunctionType { - // Result type of the function. - Type result_type = 1; - - // Argument types of the function. - repeated Type arg_types = 2; - } - - // Application defined abstract type. - message AbstractType { - // The fully qualified name of this abstract type. - string name = 1; - - // Parameter types for this abstract type. - repeated Type parameter_types = 2; - } - - // CEL primitive types. - enum PrimitiveType { - // Unspecified type. - PRIMITIVE_TYPE_UNSPECIFIED = 0; - - // Boolean type. - BOOL = 1; - - // Int64 type. - // - // Proto-based integer values are widened to int64. - INT64 = 2; - - // Uint64 type. - // - // Proto-based unsigned integer values are widened to uint64. - UINT64 = 3; - - // Double type. - // - // Proto-based float values are widened to double values. - DOUBLE = 4; - - // String type. - STRING = 5; - - // Bytes type. - BYTES = 6; - } - - // Well-known protobuf types treated with first-class support in CEL. - enum WellKnownType { - // Unspecified type. - WELL_KNOWN_TYPE_UNSPECIFIED = 0; - - // Well-known protobuf.Any type. - // - // Any types are a polymorphic message type. During type-checking they are - // treated like `DYN` types, but at runtime they are resolved to a specific - // message type specified at evaluation time. - ANY = 1; - - // Well-known protobuf.Timestamp type, internally referenced as `timestamp`. - TIMESTAMP = 2; - - // Well-known protobuf.Duration type, internally referenced as `duration`. - DURATION = 3; - } - - // The kind of type. - oneof type_kind { - // Dynamic type. - google.protobuf.Empty dyn = 1; - - // Null value. - google.protobuf.NullValue null = 2; - - // Primitive types: `true`, `1u`, `-2.0`, `'string'`, `b'bytes'`. - PrimitiveType primitive = 3; - - // Wrapper of a primitive type, e.g. `google.protobuf.Int64Value`. - PrimitiveType wrapper = 4; - - // Well-known protobuf type such as `google.protobuf.Timestamp`. - WellKnownType well_known = 5; - - // Parameterized list with elements of `list_type`, e.g. `list`. - ListType list_type = 6; - - // Parameterized map with typed keys and values. - MapType map_type = 7; - - // Function type. - FunctionType function = 8; - - // Protocol buffer message type. - // - // The `message_type` string specifies the qualified message type name. For - // example, `google.plus.Profile`. - string message_type = 9; - - // Type param type. - // - // The `type_param` string specifies the type parameter name, e.g. `list` - // would be a `list_type` whose element type was a `type_param` type - // named `E`. - string type_param = 10; - - // Type type. - // - // The `type` value specifies the target type. e.g. int is type with a - // target type of `Primitive.INT`. - Type type = 11; - - // Error type. - // - // During type-checking if an expression is an error, its type is propagated - // as the `ERROR` type. This permits the type-checker to discover other - // errors present in the expression. - google.protobuf.Empty error = 12; - - // Abstract, application defined type. - AbstractType abstract_type = 14; - } -} - -// Represents a declaration of a named value or function. -// -// A declaration is part of the contract between the expression, the agent -// evaluating that expression, and the caller requesting evaluation. -message Decl { - // Identifier declaration which specifies its type and optional `Expr` value. - // - // An identifier without a value is a declaration that must be provided at - // evaluation time. An identifier with a value should resolve to a constant, - // but may be used in conjunction with other identifiers bound at evaluation - // time. - message IdentDecl { - // Required. The type of the identifier. - Type type = 1; - - // The constant value of the identifier. If not specified, the identifier - // must be supplied at evaluation time. - Constant value = 2; - - // Documentation string for the identifier. - string doc = 3; - } - - // Function declaration specifies one or more overloads which indicate the - // function's parameter types and return type, and may optionally specify a - // function definition in terms of CEL expressions. - // - // Functions have no observable side-effects (there may be side-effects like - // logging which are not observable from CEL). - message FunctionDecl { - // An overload indicates a function's parameter types and return type, and - // may optionally include a function body described in terms of - // [Expr][google.api.expr.v1alpha1.Expr] values. - // - // Functions overloads are declared in either a function or method - // call-style. For methods, the `params[0]` is the expected type of the - // target receiver. - // - // Overloads must have non-overlapping argument types after erasure of all - // parameterized type variables (similar as type erasure in Java). - message Overload { - // Required. Globally unique overload name of the function which reflects - // the function name and argument types. - // - // This will be used by a [Reference][google.api.expr.v1alpha1.Reference] - // to indicate the `overload_id` that was resolved for the function - // `name`. - string overload_id = 1; - - // List of function parameter [Type][google.api.expr.v1alpha1.Type] - // values. - // - // Param types are disjoint after generic type parameters have been - // replaced with the type `DYN`. Since the `DYN` type is compatible with - // any other type, this means that if `A` is a type parameter, the - // function types `int
` and `int` are not disjoint. Likewise, - // `map` is not disjoint from `map`. - // - // When the `result_type` of a function is a generic type param, the - // type param name also appears as the `type` of on at least one params. - repeated Type params = 2; - - // The type param names associated with the function declaration. - // - // For example, `function ex(K key, map map) : V` would yield - // the type params of `K, V`. - repeated string type_params = 3; - - // Required. The result type of the function. For example, the operator - // `string.isEmpty()` would have `result_type` of `kind: BOOL`. - Type result_type = 4; - - // Whether the function is to be used in a method call-style `x.f(...)` - // of a function call-style `f(x, ...)`. - // - // For methods, the first parameter declaration, `params[0]` is the - // expected type of the target receiver. - bool is_instance_function = 5; - - // Documentation string for the overload. - string doc = 6; - } - - // Required. List of function overloads, must contain at least one overload. - repeated Overload overloads = 1; - } - - // The fully qualified name of the declaration. - // - // Declarations are organized in containers and this represents the full path - // to the declaration in its container, as in `google.api.expr.Decl`. - // - // Declarations used as - // [FunctionDecl.Overload][google.api.expr.v1alpha1.Decl.FunctionDecl.Overload] - // parameters may or may not have a name depending on whether the overload is - // function declaration or a function definition containing a result - // [Expr][google.api.expr.v1alpha1.Expr]. - string name = 1; - - // Required. The declaration kind. - oneof decl_kind { - // Identifier declaration. - IdentDecl ident = 2; - - // Function declaration. - FunctionDecl function = 3; - } -} - -// Describes a resolved reference to a declaration. -message Reference { - // The fully qualified name of the declaration. - string name = 1; - - // For references to functions, this is a list of `Overload.overload_id` - // values which match according to typing rules. - // - // If the list has more than one element, overload resolution among the - // presented candidates must happen at runtime because of dynamic types. The - // type checker attempts to narrow down this list as much as possible. - // - // Empty if this is not a reference to a - // [Decl.FunctionDecl][google.api.expr.v1alpha1.Decl.FunctionDecl]. - repeated string overload_id = 3; - - // For references to constants, this may contain the value of the - // constant if known at compile time. - Constant value = 4; -} diff --git a/third_party/google/api/expr/v1alpha1/conformance_service.proto b/third_party/google/api/expr/v1alpha1/conformance_service.proto deleted file mode 100644 index 7a9321a0e..000000000 --- a/third_party/google/api/expr/v1alpha1/conformance_service.proto +++ /dev/null @@ -1,165 +0,0 @@ -// Copyright 2018 Google LLC. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -// - -syntax = "proto3"; - -package google.api.expr.v1alpha1; - -import "google/api/expr/v1alpha1/checked.proto"; -import "google/api/expr/v1alpha1/eval.proto"; -import "google/api/expr/v1alpha1/syntax.proto"; -import "google/rpc/status.proto"; - -option cc_enable_arenas = true; -option go_package = "google.golang.org/genproto/googleapis/api/expr/v1alpha1;expr"; -option java_multiple_files = true; -option java_outer_classname = "ConformanceServiceProto"; -option java_package = "com.google.api.expr.v1alpha1"; - -// Access a CEL implementation from another process or machine. -// A CEL implementation is decomposed as a parser, a static checker, -// and an evaluator. Every CEL implementation is expected to provide -// a server for this API. The API will be used for conformance testing -// and other utilities. -service ConformanceService { - // Transforms CEL source text into a parsed representation. - rpc Parse(ParseRequest) returns (ParseResponse) {} - - // Runs static checks on a parsed CEL representation and return - // an annotated representation, or a set of issues. - rpc Check(CheckRequest) returns (CheckResponse) {} - - // Evaluates a parsed or annotation CEL representation given - // values of external bindings. - rpc Eval(EvalRequest) returns (EvalResponse) {} -} - -// Request message for the Parse method. -message ParseRequest { - // Required. Source text in CEL syntax. - string cel_source = 1; - - // Tag for version of CEL syntax, for future use. - string syntax_version = 2; - - // File or resource for source text, used in - // [SourceInfo][google.api.expr.v1alpha1.SourceInfo]. - string source_location = 3; - - // Prevent macro expansion. See "Macros" in Language Defiinition. - bool disable_macros = 4; -} - -// Response message for the Parse method. -message ParseResponse { - // The parsed representation, or unset if parsing failed. - ParsedExpr parsed_expr = 1; - - // Any number of issues with [StatusDetails][] as the details. - repeated google.rpc.Status issues = 2; -} - -// Request message for the Check method. -message CheckRequest { - // Required. The parsed representation of the CEL program. - ParsedExpr parsed_expr = 1; - - // Declarations of types for external variables and functions. - // Required if program uses external variables or functions - // not in the default environment. - repeated Decl type_env = 2; - - // The protocol buffer context. See "Name Resolution" in the - // Language Definition. - string container = 3; - - // If true, use only the declarations in - // [type_env][google.api.expr.v1alpha1.CheckRequest.type_env]. If false - // (default), add declarations for the standard definitions to the type - // environment. See "Standard Definitions" in the Language Definition. - bool no_std_env = 4; -} - -// Response message for the Check method. -message CheckResponse { - // The annotated representation, or unset if checking failed. - CheckedExpr checked_expr = 1; - - // Any number of issues with [StatusDetails][] as the details. - repeated google.rpc.Status issues = 2; -} - -// Request message for the Eval method. -message EvalRequest { - // Required. Either the parsed or annotated representation of the CEL program. - oneof expr_kind { - // Evaluate based on the parsed representation. - ParsedExpr parsed_expr = 1; - - // Evaluate based on the checked representation. - CheckedExpr checked_expr = 2; - } - - // Bindings for the external variables. The types SHOULD be compatible - // with the type environment in - // [CheckRequest][google.api.expr.v1alpha1.CheckRequest], if checked. - map bindings = 3; - - // SHOULD be the same container as used in - // [CheckRequest][google.api.expr.v1alpha1.CheckRequest], if checked. - string container = 4; -} - -// Response message for the Eval method. -message EvalResponse { - // The execution result, or unset if execution couldn't start. - ExprValue result = 1; - - // Any number of issues with [StatusDetails][] as the details. - // Note that CEL execution errors are reified into - // [ExprValue][google.api.expr.v1alpha1.ExprValue]. Nevertheless, we'll allow - // out-of-band issues to be raised, which also makes the replies more regular. - repeated google.rpc.Status issues = 2; -} - -// Warnings or errors in service execution are represented by -// [google.rpc.Status][google.rpc.Status] messages, with the following message -// in the details field. -message IssueDetails { - // Severities of issues. - enum Severity { - // An unspecified severity. - SEVERITY_UNSPECIFIED = 0; - - // Deprecation issue for statements and method that may no longer be - // supported or maintained. - DEPRECATION = 1; - - // Warnings such as: unused variables. - WARNING = 2; - - // Errors such as: unmatched curly braces or variable redefinition. - ERROR = 3; - } - - // The severity of the issue. - Severity severity = 1; - - // Position in the source, if known. - SourcePosition position = 2; - - // Expression ID from [Expr][google.api.expr.v1alpha1.Expr], 0 if unknown. - int64 id = 3; -} diff --git a/third_party/google/api/expr/v1alpha1/eval.proto b/third_party/google/api/expr/v1alpha1/eval.proto deleted file mode 100644 index f516ba6bc..000000000 --- a/third_party/google/api/expr/v1alpha1/eval.proto +++ /dev/null @@ -1,119 +0,0 @@ -// Copyright 2018 Google LLC. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -// - -syntax = "proto3"; - -package google.api.expr.v1alpha1; - -import "google/api/expr/v1alpha1/value.proto"; -import "google/rpc/status.proto"; - -option cc_enable_arenas = true; -option go_package = "google.golang.org/genproto/googleapis/api/expr/v1alpha1;expr"; -option java_multiple_files = true; -option java_outer_classname = "EvalProto"; -option java_package = "com.google.api.expr.v1alpha1"; - -// The state of an evaluation. -// -// Can represent an inital, partial, or completed state of evaluation. -message EvalState { - // A single evalution result. - message Result { - // The id of the expression this result if for. - int64 expr = 1; - - // The index in `values` of the resulting value. - int64 value = 2; - } - - // The unique values referenced in this message. - repeated ExprValue values = 1; - - // An ordered list of results. - // - // Tracks the flow of evaluation through the expression. - // May be sparse. - repeated Result results = 3; -} - -// The value of an evaluated expression. -message ExprValue { - // An expression can resolve to a value, error or unknown. - oneof kind { - // A concrete value. - Value value = 1; - - // The set of errors in the critical path of evalution. - // - // Only errors in the critical path are included. For example, - // `( || true) && ` will only result in ``, - // while ` || ` will result in both `` and - // ``. - // - // Errors cause by the presence of other errors are not included in the - // set. For example `.foo`, `foo()`, and ` + 1` will - // only result in ``. - // - // Multiple errors *might* be included when evaluation could result - // in different errors. For example ` + ` and - // `foo(, )` may result in ``, `` or both. - // The exact subset of errors included for this case is unspecified and - // depends on the implementation details of the evaluator. - ErrorSet error = 2; - - // The set of unknowns in the critical path of evaluation. - // - // Unknown behaves identically to Error with regards to propagation. - // Specifically, only unknowns in the critical path are included, unknowns - // caused by the presence of other unknowns are not included, and multiple - // unknowns *might* be included included when evaluation could result in - // different unknowns. For example: - // - // ( || true) && -> - // || -> - // .foo -> - // foo() -> - // + -> or - // - // Unknown takes precidence over Error in cases where a `Value` can short - // circuit the result: - // - // || -> - // && -> - // - // Errors take precidence in all other cases: - // - // + -> - // foo(, ) -> - UnknownSet unknown = 3; - } -} - -// A set of errors. -// -// The errors included depend on the context. See `ExprValue.error`. -message ErrorSet { - // The errors in the set. - repeated google.rpc.Status errors = 1; -} - -// A set of expressions for which the value is unknown. -// -// The unknowns included depend on the context. See `ExprValue.unknown`. -message UnknownSet { - // The ids of the expressions with unknown values. - repeated int64 exprs = 1; -} diff --git a/third_party/google/api/expr/v1alpha1/explain.proto b/third_party/google/api/expr/v1alpha1/explain.proto deleted file mode 100644 index 089e144a1..000000000 --- a/third_party/google/api/expr/v1alpha1/explain.proto +++ /dev/null @@ -1,54 +0,0 @@ -// Copyright 2018 Google LLC. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -// - -syntax = "proto3"; - -package google.api.expr.v1alpha1; - -import "google/api/expr/v1alpha1/value.proto"; - -option cc_enable_arenas = true; -option go_package = "google.golang.org/genproto/googleapis/api/expr/v1alpha1;expr"; -option java_multiple_files = true; -option java_outer_classname = "ExplainProto"; -option java_package = "com.google.api.expr.v1alpha1"; - -// Values of intermediate expressions produced when evaluating expression. -// Deprecated, use `EvalState` instead. -message Explain { - option deprecated = true; - - // ID and value index of one step. - message ExprStep { - // ID of corresponding Expr node. - int64 id = 1; - - // Index of the value in the values list. - int32 value_index = 2; - } - - // All of the observed values. - // - // The field value_index is an index in the values list. - // Separating values from steps is needed to remove redundant values. - repeated Value values = 1; - - // List of steps. - // - // Repeated evaluations of the same expression generate new ExprStep - // instances. The order of such ExprStep instances matches the order of - // elements returned by Comprehension.iter_range. - repeated ExprStep expr_steps = 2; -} diff --git a/third_party/google/api/expr/v1alpha1/syntax.proto b/third_party/google/api/expr/v1alpha1/syntax.proto deleted file mode 100644 index 4a3cb907a..000000000 --- a/third_party/google/api/expr/v1alpha1/syntax.proto +++ /dev/null @@ -1,322 +0,0 @@ -// Copyright 2018 Google LLC. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -// - -syntax = "proto3"; - -package google.api.expr.v1alpha1; - -import "google/protobuf/duration.proto"; -import "google/protobuf/struct.proto"; -import "google/protobuf/timestamp.proto"; - -option cc_enable_arenas = true; -option go_package = "google.golang.org/genproto/googleapis/api/expr/v1alpha1;expr"; -option java_multiple_files = true; -option java_outer_classname = "SyntaxProto"; -option java_package = "com.google.api.expr.v1alpha1"; - -// A representation of the abstract syntax of the Common Expression Language. - -// An expression together with source information as returned by the parser. -message ParsedExpr { - // The parsed expression. - Expr expr = 2; - - // The source info derived from input that generated the parsed `expr`. - SourceInfo source_info = 3; -} - -// An abstract representation of a common expression. -// -// Expressions are abstractly represented as a collection of identifiers, -// select statements, function calls, literals, and comprehensions. All -// operators with the exception of the '.' operator are modelled as function -// calls. This makes it easy to represent new operators into the existing AST. -// -// All references within expressions must resolve to a -// [Decl][google.api.expr.v1alpha1.Decl] provided at type-check for an -// expression to be valid. A reference may either be a bare identifier `name` or -// a qualified identifier `google.api.name`. References may either refer to a -// value or a function declaration. -// -// For example, the expression `google.api.name.startsWith('expr')` references -// the declaration `google.api.name` within a -// [Expr.Select][google.api.expr.v1alpha1.Expr.Select] expression, and the -// function declaration `startsWith`. -message Expr { - // An identifier expression. e.g. `request`. - message Ident { - // Required. Holds a single, unqualified identifier, possibly preceded by a - // '.'. - // - // Qualified names are represented by the - // [Expr.Select][google.api.expr.v1alpha1.Expr.Select] expression. - string name = 1; - } - - // A field selection expression. e.g. `request.auth`. - message Select { - // Required. The target of the selection expression. - // - // For example, in the select expression `request.auth`, the `request` - // portion of the expression is the `operand`. - Expr operand = 1; - - // Required. The name of the field to select. - // - // For example, in the select expression `request.auth`, the `auth` portion - // of the expression would be the `field`. - string field = 2; - - // Whether the select is to be interpreted as a field presence test. - // - // This results from the macro `has(request.auth)`. - bool test_only = 3; - } - - // A call expression, including calls to predefined functions and operators. - // - // For example, `value == 10`, `size(map_value)`. - message Call { - // The target of an method call-style expression. For example, `x` in - // `x.f()`. - Expr target = 1; - - // Required. The name of the function or method being called. - string function = 2; - - // The arguments. - repeated Expr args = 3; - } - - // A list creation expression. - // - // Lists may either be homogenous, e.g. `[1, 2, 3]`, or heterogenous, e.g. - // `dyn([1, 'hello', 2.0])` - message CreateList { - // The elements part of the list. - repeated Expr elements = 1; - } - - // A map or message creation expression. - // - // Maps are constructed as `{'key_name': 'value'}`. Message construction is - // similar, but prefixed with a type name and composed of field ids: - // `types.MyType{field_id: 'value'}`. - message CreateStruct { - // Represents an entry. - message Entry { - // Required. An id assigned to this node by the parser which is unique - // in a given expression tree. This is used to associate type - // information and other attributes to the node. - int64 id = 1; - - // The `Entry` key kinds. - oneof key_kind { - // The field key for a message creator statement. - string field_key = 2; - - // The key expression for a map creation statement. - Expr map_key = 3; - } - - // Required. The value assigned to the key. - Expr value = 4; - } - - // The type name of the message to be created, empty when creating map - // literals. - string message_name = 1; - - // The entries in the creation expression. - repeated Entry entries = 2; - } - - // A comprehension expression applied to a list or map. - // - // Comprehensions are not part of the core syntax, but enabled with macros. - // A macro matches a specific call signature within a parsed AST and replaces - // the call with an alternate AST block. Macro expansion happens at parse - // time. - // - // The following macros are supported within CEL: - // - // Aggregate type macros may be applied to all elements in a list or all keys - // in a map: - // - // * `all`, `exists`, `exists_one` - test a predicate expression against - // the inputs and return `true` if the predicate is satisfied for all, - // any, or only one value `list.all(x, x < 10)`. - // * `filter` - test a predicate expression against the inputs and return - // the subset of elements which satisfy the predicate: - // `payments.filter(p, p > 1000)`. - // * `map` - apply an expression to all elements in the input and return the - // output aggregate type: `[1, 2, 3].map(i, i * i)`. - // - // The `has(m.x)` macro tests whether the property `x` is present in struct - // `m`. The semantics of this macro depend on the type of `m`. For proto2 - // messages `has(m.x)` is defined as 'defined, but not set`. For proto3, the - // macro tests whether the property is set to its default. For map and struct - // types, the macro tests whether the property `x` is defined on `m`. - message Comprehension { - // The name of the iteration variable. - string iter_var = 1; - - // The range over which var iterates. - Expr iter_range = 2; - - // The name of the variable used for accumulation of the result. - string accu_var = 3; - - // The initial value of the accumulator. - Expr accu_init = 4; - - // An expression which can contain iter_var and accu_var. - // - // Returns false when the result has been computed and may be used as - // a hint to short-circuit the remainder of the comprehension. - Expr loop_condition = 5; - - // An expression which can contain iter_var and accu_var. - // - // Computes the next value of accu_var. - Expr loop_step = 6; - - // An expression which can contain accu_var. - // - // Computes the result. - Expr result = 7; - } - - // Required. An id assigned to this node by the parser which is unique in a - // given expression tree. This is used to associate type information and other - // attributes to a node in the parse tree. - int64 id = 2; - - // Required. Variants of expressions. - oneof expr_kind { - // A literal expression. - Constant const_expr = 3; - - // An identifier expression. - Ident ident_expr = 4; - - // A field selection expression, e.g. `request.auth`. - Select select_expr = 5; - - // A call expression, including calls to predefined functions and operators. - Call call_expr = 6; - - // A list creation expression. - CreateList list_expr = 7; - - // A map or message creation expression. - CreateStruct struct_expr = 8; - - // A comprehension expression. - Comprehension comprehension_expr = 9; - } -} - -// Represents a primitive literal. -// -// Named 'Constant' here for backwards compatibility. -// -// This is similar as the primitives supported in the well-known type -// `google.protobuf.Value`, but richer so it can represent CEL's full range of -// primitives. -// -// Lists and structs are not included as constants as these aggregate types may -// contain [Expr][google.api.expr.v1alpha1.Expr] elements which require -// evaluation and are thus not constant. -// -// Examples of literals include: `"hello"`, `b'bytes'`, `1u`, `4.2`, `-2`, -// `true`, `null`. -message Constant { - // Required. The valid constant kinds. - oneof constant_kind { - // null value. - google.protobuf.NullValue null_value = 1; - - // boolean value. - bool bool_value = 2; - - // int64 value. - int64 int64_value = 3; - - // uint64 value. - uint64 uint64_value = 4; - - // double value. - double double_value = 5; - - // string value. - string string_value = 6; - - // bytes value. - bytes bytes_value = 7; - - // protobuf.Duration value. - // - // Deprecated: duration is no longer considered a builtin cel type. - google.protobuf.Duration duration_value = 8 [deprecated = true]; - - // protobuf.Timestamp value. - // - // Deprecated: timestamp is no longer considered a builtin cel type. - google.protobuf.Timestamp timestamp_value = 9 [deprecated = true]; - } -} - -// Source information collected at parse time. -message SourceInfo { - // The syntax version of the source, e.g. `cel1`. - string syntax_version = 1; - - // The location name. All position information attached to an expression is - // relative to this location. - // - // The location could be a file, UI element, or similar. For example, - // `acme/app/AnvilPolicy.cel`. - string location = 2; - - // Monotonically increasing list of character offsets where newlines appear. - // - // The line number of a given position is the index `i` where for a given - // `id` the `line_offsets[i] < id_positions[id] < line_offsets[i+1]`. The - // column may be derivd from `id_positions[id] - line_offsets[i]`. - repeated int32 line_offsets = 3; - - // A map from the parse node id (e.g. `Expr.id`) to the character offset - // within source. - map positions = 4; -} - -// A specific position in source. -message SourcePosition { - // The soucre location name (e.g. file name). - string location = 1; - - // The character offset. - int32 offset = 2; - - // The 1-based index of the starting line in the source text - // where the issue occurs, or 0 if unknown. - int32 line = 3; - - // The 0-based index of the starting position within the line of source text - // where the issue occurs. Only meaningful if line is nonzero. - int32 column = 4; -} diff --git a/third_party/google/api/expr/v1alpha1/value.proto b/third_party/google/api/expr/v1alpha1/value.proto deleted file mode 100644 index a0508ed91..000000000 --- a/third_party/google/api/expr/v1alpha1/value.proto +++ /dev/null @@ -1,116 +0,0 @@ -// Copyright 2018 Google LLC. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -// - -syntax = "proto3"; - -package google.api.expr.v1alpha1; - -import "google/protobuf/any.proto"; -import "google/protobuf/struct.proto"; - -option cc_enable_arenas = true; -option go_package = "google.golang.org/genproto/googleapis/api/expr/v1alpha1;expr"; -option java_multiple_files = true; -option java_outer_classname = "ValueProto"; -option java_package = "com.google.api.expr.v1alpha1"; - -// Contains representations for CEL runtime values. - -// Represents a CEL value. -// -// This is similar to `google.protobuf.Value`, but can represent CEL's full -// range of values. -message Value { - // Required. The valid kinds of values. - oneof kind { - // Null value. - google.protobuf.NullValue null_value = 1; - - // Boolean value. - bool bool_value = 2; - - // Signed integer value. - int64 int64_value = 3; - - // Unsigned integer value. - uint64 uint64_value = 4; - - // Floating point value. - double double_value = 5; - - // UTF-8 string value. - string string_value = 6; - - // Byte string value. - bytes bytes_value = 7; - - // An enum value. - EnumValue enum_value = 9; - - // The proto message backing an object value. - google.protobuf.Any object_value = 10; - - // Map value. - MapValue map_value = 11; - - // List value. - ListValue list_value = 12; - - // Type value. - string type_value = 15; - } -} - -// An enum value. -message EnumValue { - // The fully qualified name of the enum type. - string type = 1; - - // The value of the enum. - int32 value = 2; -} - -// A list. -// -// Wrapped in a message so 'not set' and empty can be differentiated, which is -// required for use in a 'oneof'. -message ListValue { - // The ordered values in the list. - repeated Value values = 1; -} - -// A map. -// -// Wrapped in a message so 'not set' and empty can be differentiated, which is -// required for use in a 'oneof'. -message MapValue { - // An entry in the map. - message Entry { - // The key. - // - // Must be unique with in the map. - // Currently only boolean, int, uint, and string values can be keys. - Value key = 1; - - // The value. - Value value = 2; - } - - // The set of map entries. - // - // CEL has fewer restrictions on keys, so a protobuf map represenation - // cannot be used. - repeated Entry entries = 1; -} diff --git a/third_party/google/api/expr/v1beta1/decl.proto b/third_party/google/api/expr/v1beta1/decl.proto deleted file mode 100644 index 6d079b822..000000000 --- a/third_party/google/api/expr/v1beta1/decl.proto +++ /dev/null @@ -1,84 +0,0 @@ -// Copyright 2018 Google LLC. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -// - -syntax = "proto3"; - -package google.api.expr.v1beta1; - -import "google/api/expr/v1beta1/expr.proto"; - -option cc_enable_arenas = true; -option go_package = "google.golang.org/genproto/googleapis/api/expr/v1beta1;expr"; -option java_multiple_files = true; -option java_outer_classname = "DeclProto"; -option java_package = "com.google.api.expr.v1beta1"; - -// A declaration. -message Decl { - // The id of the declaration. - int32 id = 1; - - // The name of the declaration. - string name = 2; - - // The documentation string for the declaration. - string doc = 3; - - // The kind of declaration. - oneof kind { - // An identifier declaration. - IdentDecl ident = 4; - - // A function declaration. - FunctionDecl function = 5; - } -} - -// The declared type of a variable. -// -// Extends runtime type values with extra information used for type checking -// and dispatching. -message DeclType { - // The expression id of the declared type, if applicable. - int32 id = 1; - - // The type name, e.g. 'int', 'my.type.Type' or 'T' - string type = 2; - - // An ordered list of type parameters, e.g. ``. - // Only applies to a subset of types, e.g. `map`, `list`. - repeated DeclType type_params = 4; -} - -// An identifier declaration. -message IdentDecl { - // Optional type of the identifier. - DeclType type = 3; - - // Optional value of the identifier. - Expr value = 4; -} - -// A function declaration. -message FunctionDecl { - // The function arguments. - repeated IdentDecl args = 1; - - // Optional declared return type. - DeclType return_type = 2; - - // If the first argument of the function is the receiver. - bool receiver_function = 3; -} diff --git a/third_party/google/api/expr/v1beta1/eval.proto b/third_party/google/api/expr/v1beta1/eval.proto deleted file mode 100644 index cdbe6ac3f..000000000 --- a/third_party/google/api/expr/v1beta1/eval.proto +++ /dev/null @@ -1,125 +0,0 @@ -// Copyright 2018 Google LLC. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -// - -syntax = "proto3"; - -package google.api.expr.v1beta1; - -import "google/api/expr/v1beta1/value.proto"; -import "google/rpc/status.proto"; - -option cc_enable_arenas = true; -option go_package = "google.golang.org/genproto/googleapis/api/expr/v1beta1;expr"; -option java_multiple_files = true; -option java_outer_classname = "EvalProto"; -option java_package = "com.google.api.expr.v1beta1"; - -// The state of an evaluation. -// -// Can represent an initial, partial, or completed state of evaluation. -message EvalState { - // A single evaluation result. - message Result { - // The expression this result is for. - IdRef expr = 1; - - // The index in `values` of the resulting value. - int32 value = 2; - } - - // The unique values referenced in this message. - repeated ExprValue values = 1; - - // An ordered list of results. - // - // Tracks the flow of evaluation through the expression. - // May be sparse. - repeated Result results = 3; -} - -// The value of an evaluated expression. -message ExprValue { - // An expression can resolve to a value, error or unknown. - oneof kind { - // A concrete value. - Value value = 1; - - // The set of errors in the critical path of evalution. - // - // Only errors in the critical path are included. For example, - // `( || true) && ` will only result in ``, - // while ` || ` will result in both `` and - // ``. - // - // Errors cause by the presence of other errors are not included in the - // set. For example `.foo`, `foo()`, and ` + 1` will - // only result in ``. - // - // Multiple errors *might* be included when evaluation could result - // in different errors. For example ` + ` and - // `foo(, )` may result in ``, `` or both. - // The exact subset of errors included for this case is unspecified and - // depends on the implementation details of the evaluator. - ErrorSet error = 2; - - // The set of unknowns in the critical path of evaluation. - // - // Unknown behaves identically to Error with regards to propagation. - // Specifically, only unknowns in the critical path are included, unknowns - // caused by the presence of other unknowns are not included, and multiple - // unknowns *might* be included included when evaluation could result in - // different unknowns. For example: - // - // ( || true) && -> - // || -> - // .foo -> - // foo() -> - // + -> or - // - // Unknown takes precidence over Error in cases where a `Value` can short - // circuit the result: - // - // || -> - // && -> - // - // Errors take precidence in all other cases: - // - // + -> - // foo(, ) -> - UnknownSet unknown = 3; - } -} - -// A set of errors. -// -// The errors included depend on the context. See `ExprValue.error`. -message ErrorSet { - // The errors in the set. - repeated google.rpc.Status errors = 1; -} - -// A set of expressions for which the value is unknown. -// -// The unknowns included depend on the context. See `ExprValue.unknown`. -message UnknownSet { - // The ids of the expressions with unknown values. - repeated IdRef exprs = 1; -} - -// A reference to an expression id. -message IdRef { - // The expression id. - int32 id = 1; -} diff --git a/third_party/google/api/expr/v1beta1/expr.proto b/third_party/google/api/expr/v1beta1/expr.proto deleted file mode 100644 index 93b917f14..000000000 --- a/third_party/google/api/expr/v1beta1/expr.proto +++ /dev/null @@ -1,269 +0,0 @@ -// Copyright 2018 Google LLC. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -// - -syntax = "proto3"; - -package google.api.expr.v1beta1; - -import "google/api/expr/v1beta1/source.proto"; -import "google/protobuf/struct.proto"; - -option cc_enable_arenas = true; -option go_package = "google.golang.org/genproto/googleapis/api/expr/v1beta1;expr"; -option java_multiple_files = true; -option java_outer_classname = "ExprProto"; -option java_package = "com.google.api.expr.v1beta1"; - -// An expression together with source information as returned by the parser. -message ParsedExpr { - // The parsed expression. - Expr expr = 2; - - // The source info derived from input that generated the parsed `expr`. - SourceInfo source_info = 3; - - // The syntax version of the source, e.g. `cel1`. - string syntax_version = 4; -} - -// An abstract representation of a common expression. -// -// Expressions are abstractly represented as a collection of identifiers, -// select statements, function calls, literals, and comprehensions. All -// operators with the exception of the '.' operator are modelled as function -// calls. This makes it easy to represent new operators into the existing AST. -// -// All references within expressions must resolve to a -// [Decl][google.api.expr.v1beta1.Decl] provided at type-check for an expression -// to be valid. A reference may either be a bare identifier `name` or a -// qualified identifier `google.api.name`. References may either refer to a -// value or a function declaration. -// -// For example, the expression `google.api.name.startsWith('expr')` references -// the declaration `google.api.name` within a -// [Expr.Select][google.api.expr.v1beta1.Expr.Select] expression, and the -// function declaration `startsWith`. -message Expr { - // An identifier expression. e.g. `request`. - message Ident { - // Required. Holds a single, unqualified identifier, possibly preceded by a - // '.'. - // - // Qualified names are represented by the - // [Expr.Select][google.api.expr.v1beta1.Expr.Select] expression. - string name = 1; - } - - // A field selection expression. e.g. `request.auth`. - message Select { - // Required. The target of the selection expression. - // - // For example, in the select expression `request.auth`, the `request` - // portion of the expression is the `operand`. - Expr operand = 1; - - // Required. The name of the field to select. - // - // For example, in the select expression `request.auth`, the `auth` portion - // of the expression would be the `field`. - string field = 2; - - // Whether the select is to be interpreted as a field presence test. - // - // This results from the macro `has(request.auth)`. - bool test_only = 3; - } - - // A call expression, including calls to predefined functions and operators. - // - // For example, `value == 10`, `size(map_value)`. - message Call { - // The target of an method call-style expression. For example, `x` in - // `x.f()`. - Expr target = 1; - - // Required. The name of the function or method being called. - string function = 2; - - // The arguments. - repeated Expr args = 3; - } - - // A list creation expression. - // - // Lists may either be homogenous, e.g. `[1, 2, 3]`, or heterogenous, e.g. - // `dyn([1, 'hello', 2.0])` - message CreateList { - // The elements part of the list. - repeated Expr elements = 1; - } - - // A map or message creation expression. - // - // Maps are constructed as `{'key_name': 'value'}`. Message construction is - // similar, but prefixed with a type name and composed of field ids: - // `types.MyType{field_id: 'value'}`. - message CreateStruct { - // Represents an entry. - message Entry { - // Required. An id assigned to this node by the parser which is unique - // in a given expression tree. This is used to associate type - // information and other attributes to the node. - int32 id = 1; - - // The `Entry` key kinds. - oneof key_kind { - // The field key for a message creator statement. - string field_key = 2; - - // The key expression for a map creation statement. - Expr map_key = 3; - } - - // Required. The value assigned to the key. - Expr value = 4; - } - - // The type name of the message to be created, empty when creating map - // literals. - string type = 1; - - // The entries in the creation expression. - repeated Entry entries = 2; - } - - // A comprehension expression applied to a list or map. - // - // Comprehensions are not part of the core syntax, but enabled with macros. - // A macro matches a specific call signature within a parsed AST and replaces - // the call with an alternate AST block. Macro expansion happens at parse - // time. - // - // The following macros are supported within CEL: - // - // Aggregate type macros may be applied to all elements in a list or all keys - // in a map: - // - // * `all`, `exists`, `exists_one` - test a predicate expression against - // the inputs and return `true` if the predicate is satisfied for all, - // any, or only one value `list.all(x, x < 10)`. - // * `filter` - test a predicate expression against the inputs and return - // the subset of elements which satisfy the predicate: - // `payments.filter(p, p > 1000)`. - // * `map` - apply an expression to all elements in the input and return the - // output aggregate type: `[1, 2, 3].map(i, i * i)`. - // - // The `has(m.x)` macro tests whether the property `x` is present in struct - // `m`. The semantics of this macro depend on the type of `m`. For proto2 - // messages `has(m.x)` is defined as 'defined, but not set`. For proto3, the - // macro tests whether the property is set to its default. For map and struct - // types, the macro tests whether the property `x` is defined on `m`. - message Comprehension { - // The name of the iteration variable. - string iter_var = 1; - - // The range over which var iterates. - Expr iter_range = 2; - - // The name of the variable used for accumulation of the result. - string accu_var = 3; - - // The initial value of the accumulator. - Expr accu_init = 4; - - // An expression which can contain iter_var and accu_var. - // - // Returns false when the result has been computed and may be used as - // a hint to short-circuit the remainder of the comprehension. - Expr loop_condition = 5; - - // An expression which can contain iter_var and accu_var. - // - // Computes the next value of accu_var. - Expr loop_step = 6; - - // An expression which can contain accu_var. - // - // Computes the result. - Expr result = 7; - } - - // Required. An id assigned to this node by the parser which is unique in a - // given expression tree. This is used to associate type information and other - // attributes to a node in the parse tree. - int32 id = 2; - - // Required. Variants of expressions. - oneof expr_kind { - // A literal expression. - Literal literal_expr = 3; - - // An identifier expression. - Ident ident_expr = 4; - - // A field selection expression, e.g. `request.auth`. - Select select_expr = 5; - - // A call expression, including calls to predefined functions and operators. - Call call_expr = 6; - - // A list creation expression. - CreateList list_expr = 7; - - // A map or object creation expression. - CreateStruct struct_expr = 8; - - // A comprehension expression. - Comprehension comprehension_expr = 9; - } -} - -// Represents a primitive literal. -// -// This is similar to the primitives supported in the well-known type -// `google.protobuf.Value`, but richer so it can represent CEL's full range of -// primitives. -// -// Lists and structs are not included as constants as these aggregate types may -// contain [Expr][google.api.expr.v1beta1.Expr] elements which require -// evaluation and are thus not constant. -// -// Examples of literals include: `"hello"`, `b'bytes'`, `1u`, `4.2`, `-2`, -// `true`, `null`. -message Literal { - // Required. The valid constant kinds. - oneof constant_kind { - // null value. - google.protobuf.NullValue null_value = 1; - - // boolean value. - bool bool_value = 2; - - // int64 value. - int64 int64_value = 3; - - // uint64 value. - uint64 uint64_value = 4; - - // double value. - double double_value = 5; - - // string value. - string string_value = 6; - - // bytes value. - bytes bytes_value = 7; - } -} diff --git a/third_party/google/api/expr/v1beta1/source.proto b/third_party/google/api/expr/v1beta1/source.proto deleted file mode 100644 index adaf84d5e..000000000 --- a/third_party/google/api/expr/v1beta1/source.proto +++ /dev/null @@ -1,62 +0,0 @@ -// Copyright 2018 Google LLC. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -// - -syntax = "proto3"; - -package google.api.expr.v1beta1; - -option cc_enable_arenas = true; -option go_package = "google.golang.org/genproto/googleapis/api/expr/v1beta1;expr"; -option java_multiple_files = true; -option java_outer_classname = "SourceProto"; -option java_package = "com.google.api.expr.v1beta1"; - -// Source information collected at parse time. -message SourceInfo { - // The location name. All position information attached to an expression is - // relative to this location. - // - // The location could be a file, UI element, or similar. For example, - // `acme/app/AnvilPolicy.cel`. - string location = 2; - - // Monotonically increasing list of character offsets where newlines appear. - // - // The line number of a given position is the index `i` where for a given - // `id` the `line_offsets[i] < id_positions[id] < line_offsets[i+1]`. The - // column may be derivd from `id_positions[id] - line_offsets[i]`. - repeated int32 line_offsets = 3; - - // A map from the parse node id (e.g. `Expr.id`) to the character offset - // within source. - map positions = 4; -} - -// A specific position in source. -message SourcePosition { - // The soucre location name (e.g. file name). - string location = 1; - - // The character offset. - int32 offset = 2; - - // The 1-based index of the starting line in the source text - // where the issue occurs, or 0 if unknown. - int32 line = 3; - - // The 0-based index of the starting position within the line of source text - // where the issue occurs. Only meaningful if line is nonzer.. - int32 column = 4; -} diff --git a/third_party/google/api/expr/v1beta1/value.proto b/third_party/google/api/expr/v1beta1/value.proto deleted file mode 100644 index a5ae06766..000000000 --- a/third_party/google/api/expr/v1beta1/value.proto +++ /dev/null @@ -1,114 +0,0 @@ -// Copyright 2018 Google LLC. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -// - -syntax = "proto3"; - -package google.api.expr.v1beta1; - -import "google/protobuf/any.proto"; -import "google/protobuf/struct.proto"; - -option cc_enable_arenas = true; -option go_package = "google.golang.org/genproto/googleapis/api/expr/v1beta1;expr"; -option java_multiple_files = true; -option java_outer_classname = "ValueProto"; -option java_package = "com.google.api.expr.v1beta1"; - -// Represents a CEL value. -// -// This is similar to `google.protobuf.Value`, but can represent CEL's full -// range of values. -message Value { - // Required. The valid kinds of values. - oneof kind { - // Null value. - google.protobuf.NullValue null_value = 1; - - // Boolean value. - bool bool_value = 2; - - // Signed integer value. - int64 int64_value = 3; - - // Unsigned integer value. - uint64 uint64_value = 4; - - // Floating point value. - double double_value = 5; - - // UTF-8 string value. - string string_value = 6; - - // Byte string value. - bytes bytes_value = 7; - - // An enum value. - EnumValue enum_value = 9; - - // The proto message backing an object value. - google.protobuf.Any object_value = 10; - - // Map value. - MapValue map_value = 11; - - // List value. - ListValue list_value = 12; - - // A Type value represented by the fully qualified name of the type. - string type_value = 15; - } -} - -// An enum value. -message EnumValue { - // The fully qualified name of the enum type. - string type = 1; - - // The value of the enum. - int32 value = 2; -} - -// A list. -// -// Wrapped in a message so 'not set' and empty can be differentiated, which is -// required for use in a 'oneof'. -message ListValue { - // The ordered values in the list. - repeated Value values = 1; -} - -// A map. -// -// Wrapped in a message so 'not set' and empty can be differentiated, which is -// required for use in a 'oneof'. -message MapValue { - // An entry in the map. - message Entry { - // The key. - // - // Must be unique with in the map. - // Currently only boolean, int, uint, and string values can be keys. - Value key = 1; - - // The value. - Value value = 2; - } - - // The set of map entries. - // - // CEL has fewer restrictions on keys, so a protobuf map represenation - // cannot be used. - repeated Entry entries = 1; -} diff --git a/third_party/google/api/field_behavior.proto b/third_party/google/api/field_behavior.proto deleted file mode 100644 index eb7f78ef1..000000000 --- a/third_party/google/api/field_behavior.proto +++ /dev/null @@ -1,79 +0,0 @@ -// Copyright 2019 Google LLC. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -// - -syntax = "proto3"; - -package google.api; - -import "google/protobuf/descriptor.proto"; - -option go_package = "google.golang.org/genproto/googleapis/api/annotations;annotations"; -option java_multiple_files = true; -option java_outer_classname = "FieldBehaviorProto"; -option java_package = "com.google.api"; -option objc_class_prefix = "GAPI"; - -extend google.protobuf.FieldOptions { - // A designation of a specific field behavior (required, output only, etc.) - // in protobuf messages. - // - // Examples: - // - // string name = 1 [(google.api.field_behavior) = REQUIRED]; - // State state = 1 [(google.api.field_behavior) = OUTPUT_ONLY]; - // google.protobuf.Duration ttl = 1 - // [(google.api.field_behavior) = INPUT_ONLY]; - // google.protobuf.Timestamp expire_time = 1 - // [(google.api.field_behavior) = OUTPUT_ONLY, - // (google.api.field_behavior) = IMMUTABLE]; - repeated google.api.FieldBehavior field_behavior = 1052; -} - -// An indicator of the behavior of a given field (for example, that a field -// is required in requests, or given as output but ignored as input). -// This **does not** change the behavior in protocol buffers itself; it only -// denotes the behavior and may affect how API tooling handles the field. -// -// Note: This enum **may** receive new values in the future. -enum FieldBehavior { - // Conventional default for enums. Do not use this. - FIELD_BEHAVIOR_UNSPECIFIED = 0; - - // Specifically denotes a field as optional. - // While all fields in protocol buffers are optional, this may be specified - // for emphasis if appropriate. - OPTIONAL = 1; - - // Denotes a field as required. - // This indicates that the field **must** be provided as part of the request, - // and failure to do so will cause an error (usually `INVALID_ARGUMENT`). - REQUIRED = 2; - - // Denotes a field as output only. - // This indicates that the field is provided in responses, but including the - // field in a request does nothing (the server *must* ignore it and - // *must not* throw an error as a result of the field's presence). - OUTPUT_ONLY = 3; - - // Denotes a field as input only. - // This indicates that the field is provided in requests, and the - // corresponding field is not included in output. - INPUT_ONLY = 4; - - // Denotes a field as immutable. - // This indicates that the field may be set once in a request to create a - // resource, but may not be changed thereafter. - IMMUTABLE = 5; -} diff --git a/third_party/google/api/http.proto b/third_party/google/api/http.proto index b2977f514..69460cf79 100644 --- a/third_party/google/api/http.proto +++ b/third_party/google/api/http.proto @@ -1,4 +1,4 @@ -// Copyright 2019 Google LLC. +// Copyright 2020 Google LLC // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -11,7 +11,6 @@ // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. -// syntax = "proto3"; diff --git a/third_party/google/api/httpbody.proto b/third_party/google/api/httpbody.proto index 45c1e76b1..1a5bb78be 100644 --- a/third_party/google/api/httpbody.proto +++ b/third_party/google/api/httpbody.proto @@ -1,4 +1,4 @@ -// Copyright 2019 Google LLC. +// Copyright 2020 Google LLC // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -11,7 +11,6 @@ // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. -// syntax = "proto3"; diff --git a/third_party/google/api/label.proto b/third_party/google/api/label.proto deleted file mode 100644 index 668efd1c6..000000000 --- a/third_party/google/api/label.proto +++ /dev/null @@ -1,49 +0,0 @@ -// Copyright 2019 Google LLC. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -// - -syntax = "proto3"; - -package google.api; - -option cc_enable_arenas = true; -option go_package = "google.golang.org/genproto/googleapis/api/label;label"; -option java_multiple_files = true; -option java_outer_classname = "LabelProto"; -option java_package = "com.google.api"; -option objc_class_prefix = "GAPI"; - -// A description of a label. -message LabelDescriptor { - // Value types that can be used as label values. - enum ValueType { - // A variable-length string. This is the default. - STRING = 0; - - // Boolean; true or false. - BOOL = 1; - - // A 64-bit signed integer. - INT64 = 2; - } - - // The label key. - string key = 1; - - // The type of data that can be assigned to the label. - ValueType value_type = 2; - - // A human-readable description for the label. - string description = 3; -} diff --git a/third_party/google/api/launch_stage.proto b/third_party/google/api/launch_stage.proto deleted file mode 100644 index 55fd91424..000000000 --- a/third_party/google/api/launch_stage.proto +++ /dev/null @@ -1,67 +0,0 @@ -// Copyright 2019 Google LLC. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -// - -syntax = "proto3"; - -package google.api; - -option go_package = "google.golang.org/genproto/googleapis/api;api"; -option java_multiple_files = true; -option java_outer_classname = "LaunchStageProto"; -option java_package = "com.google.api"; -option objc_class_prefix = "GAPI"; - -// The launch stage as defined by [Google Cloud Platform -// Launch Stages](http://cloud.google.com/terms/launch-stages). -enum LaunchStage { - // Do not use this default value. - LAUNCH_STAGE_UNSPECIFIED = 0; - - // Early Access features are limited to a closed group of testers. To use - // these features, you must sign up in advance and sign a Trusted Tester - // agreement (which includes confidentiality provisions). These features may - // be unstable, changed in backward-incompatible ways, and are not - // guaranteed to be released. - EARLY_ACCESS = 1; - - // Alpha is a limited availability test for releases before they are cleared - // for widespread use. By Alpha, all significant design issues are resolved - // and we are in the process of verifying functionality. Alpha customers - // need to apply for access, agree to applicable terms, and have their - // projects whitelisted. Alpha releases don’t have to be feature complete, - // no SLAs are provided, and there are no technical support obligations, but - // they will be far enough along that customers can actually use them in - // test environments or for limited-use tests -- just like they would in - // normal production cases. - ALPHA = 2; - - // Beta is the point at which we are ready to open a release for any - // customer to use. There are no SLA or technical support obligations in a - // Beta release. Products will be complete from a feature perspective, but - // may have some open outstanding issues. Beta releases are suitable for - // limited production use cases. - BETA = 3; - - // GA features are open to all developers and are considered stable and - // fully qualified for production use. - GA = 4; - - // Deprecated features are scheduled to be shut down and removed. For more - // information, see the “Deprecation Policy” section of our [Terms of - // Service](https://cloud.google.com/terms/) - // and the [Google Cloud Platform Subject to the Deprecation - // Policy](https://cloud.google.com/terms/deprecation) documentation. - DEPRECATED = 5; -} diff --git a/third_party/google/api/log.proto b/third_party/google/api/log.proto deleted file mode 100644 index 1125e1fe3..000000000 --- a/third_party/google/api/log.proto +++ /dev/null @@ -1,55 +0,0 @@ -// Copyright 2019 Google LLC. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -// - -syntax = "proto3"; - -package google.api; - -import "google/api/label.proto"; - -option go_package = "google.golang.org/genproto/googleapis/api/serviceconfig;serviceconfig"; -option java_multiple_files = true; -option java_outer_classname = "LogProto"; -option java_package = "com.google.api"; -option objc_class_prefix = "GAPI"; - -// A description of a log type. Example in YAML format: -// -// - name: library.googleapis.com/activity_history -// description: The history of borrowing and returning library items. -// display_name: Activity -// labels: -// - key: /customer_id -// description: Identifier of a library customer -message LogDescriptor { - // The name of the log. It must be less than 512 characters long and can - // include the following characters: upper- and lower-case alphanumeric - // characters [A-Za-z0-9], and punctuation characters including - // slash, underscore, hyphen, period [/_-.]. - string name = 1; - - // The set of labels that are available to describe a specific log entry. - // Runtime requests that contain labels not specified here are - // considered invalid. - repeated LabelDescriptor labels = 2; - - // A human-readable description of this log. This information appears in - // the documentation and can contain details. - string description = 3; - - // The human-readable name for this log. This information appears on - // the user interface and should be concise. - string display_name = 4; -} diff --git a/third_party/google/api/logging.proto b/third_party/google/api/logging.proto deleted file mode 100644 index 9090b2a1c..000000000 --- a/third_party/google/api/logging.proto +++ /dev/null @@ -1,81 +0,0 @@ -// Copyright 2019 Google LLC. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -// - -syntax = "proto3"; - -package google.api; - -option go_package = "google.golang.org/genproto/googleapis/api/serviceconfig;serviceconfig"; -option java_multiple_files = true; -option java_outer_classname = "LoggingProto"; -option java_package = "com.google.api"; -option objc_class_prefix = "GAPI"; - -// Logging configuration of the service. -// -// The following example shows how to configure logs to be sent to the -// producer and consumer projects. In the example, the `activity_history` -// log is sent to both the producer and consumer projects, whereas the -// `purchase_history` log is only sent to the producer project. -// -// monitored_resources: -// - type: library.googleapis.com/branch -// labels: -// - key: /city -// description: The city where the library branch is located in. -// - key: /name -// description: The name of the branch. -// logs: -// - name: activity_history -// labels: -// - key: /customer_id -// - name: purchase_history -// logging: -// producer_destinations: -// - monitored_resource: library.googleapis.com/branch -// logs: -// - activity_history -// - purchase_history -// consumer_destinations: -// - monitored_resource: library.googleapis.com/branch -// logs: -// - activity_history -message Logging { - // Configuration of a specific logging destination (the producer project - // or the consumer project). - message LoggingDestination { - // The monitored resource type. The type must be defined in the - // [Service.monitored_resources][google.api.Service.monitored_resources] section. - string monitored_resource = 3; - - // Names of the logs to be sent to this destination. Each name must - // be defined in the [Service.logs][google.api.Service.logs] section. If the log name is - // not a domain scoped name, it will be automatically prefixed with - // the service name followed by "/". - repeated string logs = 1; - } - - // Logging configurations for sending logs to the producer project. - // There can be multiple producer destinations, each one must have a - // different monitored resource type. A log can be used in at most - // one producer destination. - repeated LoggingDestination producer_destinations = 1; - - // Logging configurations for sending logs to the consumer project. - // There can be multiple consumer destinations, each one must have a - // different monitored resource type. A log can be used in at most - // one consumer destination. - repeated LoggingDestination consumer_destinations = 2; -} diff --git a/third_party/google/api/metric.proto b/third_party/google/api/metric.proto deleted file mode 100644 index 4d3c488f2..000000000 --- a/third_party/google/api/metric.proto +++ /dev/null @@ -1,215 +0,0 @@ -// Copyright 2019 Google LLC. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -// - -syntax = "proto3"; - -package google.api; - -import "google/api/label.proto"; -import "google/api/launch_stage.proto"; -import "google/protobuf/duration.proto"; - -option go_package = "google.golang.org/genproto/googleapis/api/metric;metric"; -option java_multiple_files = true; -option java_outer_classname = "MetricProto"; -option java_package = "com.google.api"; -option objc_class_prefix = "GAPI"; - -// Defines a metric type and its schema. Once a metric descriptor is created, -// deleting or altering it stops data collection and makes the metric type's -// existing data unusable. -message MetricDescriptor { - // Additional annotations that can be used to guide the usage of a metric. - message MetricDescriptorMetadata { - // The launch stage of the metric definition. - LaunchStage launch_stage = 1; - - // The sampling period of metric data points. For metrics which are written - // periodically, consecutive data points are stored at this time interval, - // excluding data loss due to errors. Metrics with a higher granularity have - // a smaller sampling period. - google.protobuf.Duration sample_period = 2; - - // The delay of data points caused by ingestion. Data points older than this - // age are guaranteed to be ingested and available to be read, excluding - // data loss due to errors. - google.protobuf.Duration ingest_delay = 3; - } - - // The kind of measurement. It describes how the data is reported. - enum MetricKind { - // Do not use this default value. - METRIC_KIND_UNSPECIFIED = 0; - - // An instantaneous measurement of a value. - GAUGE = 1; - - // The change in a value during a time interval. - DELTA = 2; - - // A value accumulated over a time interval. Cumulative - // measurements in a time series should have the same start time - // and increasing end times, until an event resets the cumulative - // value to zero and sets a new start time for the following - // points. - CUMULATIVE = 3; - } - - // The value type of a metric. - enum ValueType { - // Do not use this default value. - VALUE_TYPE_UNSPECIFIED = 0; - - // The value is a boolean. - // This value type can be used only if the metric kind is `GAUGE`. - BOOL = 1; - - // The value is a signed 64-bit integer. - INT64 = 2; - - // The value is a double precision floating point number. - DOUBLE = 3; - - // The value is a text string. - // This value type can be used only if the metric kind is `GAUGE`. - STRING = 4; - - // The value is a [`Distribution`][google.api.Distribution]. - DISTRIBUTION = 5; - - // The value is money. - MONEY = 6; - } - - // The resource name of the metric descriptor. - string name = 1; - - // The metric type, including its DNS name prefix. The type is not - // URL-encoded. All user-defined metric types have the DNS name - // `custom.googleapis.com` or `external.googleapis.com`. Metric types should - // use a natural hierarchical grouping. For example: - // - // "custom.googleapis.com/invoice/paid/amount" - // "external.googleapis.com/prometheus/up" - // "appengine.googleapis.com/http/server/response_latencies" - string type = 8; - - // The set of labels that can be used to describe a specific - // instance of this metric type. For example, the - // `appengine.googleapis.com/http/server/response_latencies` metric - // type has a label for the HTTP response code, `response_code`, so - // you can look at latencies for successful responses or just - // for responses that failed. - repeated LabelDescriptor labels = 2; - - // Whether the metric records instantaneous values, changes to a value, etc. - // Some combinations of `metric_kind` and `value_type` might not be supported. - MetricKind metric_kind = 3; - - // Whether the measurement is an integer, a floating-point number, etc. - // Some combinations of `metric_kind` and `value_type` might not be supported. - ValueType value_type = 4; - - // The unit in which the metric value is reported. It is only applicable - // if the `value_type` is `INT64`, `DOUBLE`, or `DISTRIBUTION`. The - // supported units are a subset of [The Unified Code for Units of - // Measure](http://unitsofmeasure.org/ucum.html) standard: - // - // **Basic units (UNIT)** - // - // * `bit` bit - // * `By` byte - // * `s` second - // * `min` minute - // * `h` hour - // * `d` day - // - // **Prefixes (PREFIX)** - // - // * `k` kilo (10**3) - // * `M` mega (10**6) - // * `G` giga (10**9) - // * `T` tera (10**12) - // * `P` peta (10**15) - // * `E` exa (10**18) - // * `Z` zetta (10**21) - // * `Y` yotta (10**24) - // * `m` milli (10**-3) - // * `u` micro (10**-6) - // * `n` nano (10**-9) - // * `p` pico (10**-12) - // * `f` femto (10**-15) - // * `a` atto (10**-18) - // * `z` zepto (10**-21) - // * `y` yocto (10**-24) - // * `Ki` kibi (2**10) - // * `Mi` mebi (2**20) - // * `Gi` gibi (2**30) - // * `Ti` tebi (2**40) - // - // **Grammar** - // - // The grammar also includes these connectors: - // - // * `/` division (as an infix operator, e.g. `1/s`). - // * `.` multiplication (as an infix operator, e.g. `GBy.d`) - // - // The grammar for a unit is as follows: - // - // Expression = Component { "." Component } { "/" Component } ; - // - // Component = ( [ PREFIX ] UNIT | "%" ) [ Annotation ] - // | Annotation - // | "1" - // ; - // - // Annotation = "{" NAME "}" ; - // - // Notes: - // - // * `Annotation` is just a comment if it follows a `UNIT` and is - // equivalent to `1` if it is used alone. For examples, - // `{requests}/s == 1/s`, `By{transmitted}/s == By/s`. - // * `NAME` is a sequence of non-blank printable ASCII characters not - // containing '{' or '}'. - // * `1` represents dimensionless value 1, such as in `1/s`. - // * `%` represents dimensionless value 1/100, and annotates values giving - // a percentage. - string unit = 5; - - // A detailed description of the metric, which can be used in documentation. - string description = 6; - - // A concise name for the metric, which can be displayed in user interfaces. - // Use sentence case without an ending period, for example "Request count". - // This field is optional but it is recommended to be set for any metrics - // associated with user-visible concepts, such as Quota. - string display_name = 7; - - // Optional. Metadata which can be used to guide usage of the metric. - MetricDescriptorMetadata metadata = 10; -} - -// A specific metric, identified by specifying values for all of the -// labels of a [`MetricDescriptor`][google.api.MetricDescriptor]. -message Metric { - // An existing metric type, see [google.api.MetricDescriptor][google.api.MetricDescriptor]. - // For example, `custom.googleapis.com/invoice/paid/amount`. - string type = 3; - - // The set of label values that uniquely identify this metric. All - // labels listed in the `MetricDescriptor` must be assigned values. - map labels = 2; -} diff --git a/third_party/google/api/monitored_resource.proto b/third_party/google/api/monitored_resource.proto deleted file mode 100644 index a2c4525bb..000000000 --- a/third_party/google/api/monitored_resource.proto +++ /dev/null @@ -1,115 +0,0 @@ -// Copyright 2019 Google LLC. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -// - -syntax = "proto3"; - -package google.api; - -import "google/api/label.proto"; -import "google/protobuf/struct.proto"; - -option cc_enable_arenas = true; -option go_package = "google.golang.org/genproto/googleapis/api/monitoredres;monitoredres"; -option java_multiple_files = true; -option java_outer_classname = "MonitoredResourceProto"; -option java_package = "com.google.api"; -option objc_class_prefix = "GAPI"; - -// An object that describes the schema of a [MonitoredResource][google.api.MonitoredResource] object using a -// type name and a set of labels. For example, the monitored resource -// descriptor for Google Compute Engine VM instances has a type of -// `"gce_instance"` and specifies the use of the labels `"instance_id"` and -// `"zone"` to identify particular VM instances. -// -// Different APIs can support different monitored resource types. APIs generally -// provide a `list` method that returns the monitored resource descriptors used -// by the API. -message MonitoredResourceDescriptor { - // Optional. The resource name of the monitored resource descriptor: - // `"projects/{project_id}/monitoredResourceDescriptors/{type}"` where - // {type} is the value of the `type` field in this object and - // {project_id} is a project ID that provides API-specific context for - // accessing the type. APIs that do not use project information can use the - // resource name format `"monitoredResourceDescriptors/{type}"`. - string name = 5; - - // Required. The monitored resource type. For example, the type - // `"cloudsql_database"` represents databases in Google Cloud SQL. - // The maximum length of this value is 256 characters. - string type = 1; - - // Optional. A concise name for the monitored resource type that might be - // displayed in user interfaces. It should be a Title Cased Noun Phrase, - // without any article or other determiners. For example, - // `"Google Cloud SQL Database"`. - string display_name = 2; - - // Optional. A detailed description of the monitored resource type that might - // be used in documentation. - string description = 3; - - // Required. A set of labels used to describe instances of this monitored - // resource type. For example, an individual Google Cloud SQL database is - // identified by values for the labels `"database_id"` and `"zone"`. - repeated LabelDescriptor labels = 4; -} - -// An object representing a resource that can be used for monitoring, logging, -// billing, or other purposes. Examples include virtual machine instances, -// databases, and storage devices such as disks. The `type` field identifies a -// [MonitoredResourceDescriptor][google.api.MonitoredResourceDescriptor] object that describes the resource's -// schema. Information in the `labels` field identifies the actual resource and -// its attributes according to the schema. For example, a particular Compute -// Engine VM instance could be represented by the following object, because the -// [MonitoredResourceDescriptor][google.api.MonitoredResourceDescriptor] for `"gce_instance"` has labels -// `"instance_id"` and `"zone"`: -// -// { "type": "gce_instance", -// "labels": { "instance_id": "12345678901234", -// "zone": "us-central1-a" }} -message MonitoredResource { - // Required. The monitored resource type. This field must match - // the `type` field of a [MonitoredResourceDescriptor][google.api.MonitoredResourceDescriptor] object. For - // example, the type of a Compute Engine VM instance is `gce_instance`. - string type = 1; - - // Required. Values for all of the labels listed in the associated monitored - // resource descriptor. For example, Compute Engine VM instances use the - // labels `"project_id"`, `"instance_id"`, and `"zone"`. - map labels = 2; -} - -// Auxiliary metadata for a [MonitoredResource][google.api.MonitoredResource] object. -// [MonitoredResource][google.api.MonitoredResource] objects contain the minimum set of information to -// uniquely identify a monitored resource instance. There is some other useful -// auxiliary metadata. Monitoring and Logging use an ingestion -// pipeline to extract metadata for cloud resources of all types, and store -// the metadata in this message. -message MonitoredResourceMetadata { - // Output only. Values for predefined system metadata labels. - // System labels are a kind of metadata extracted by Google, including - // "machine_image", "vpc", "subnet_id", - // "security_group", "name", etc. - // System label values can be only strings, Boolean values, or a list of - // strings. For example: - // - // { "name": "my-test-instance", - // "security_group": ["a", "b", "c"], - // "spot_instance": false } - google.protobuf.Struct system_labels = 1; - - // Output only. A map of user-defined metadata labels. - map user_labels = 2; -} diff --git a/third_party/google/api/monitoring.proto b/third_party/google/api/monitoring.proto deleted file mode 100644 index 07e962d1b..000000000 --- a/third_party/google/api/monitoring.proto +++ /dev/null @@ -1,91 +0,0 @@ -// Copyright 2019 Google LLC. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -// - -syntax = "proto3"; - -package google.api; - -option go_package = "google.golang.org/genproto/googleapis/api/serviceconfig;serviceconfig"; -option java_multiple_files = true; -option java_outer_classname = "MonitoringProto"; -option java_package = "com.google.api"; -option objc_class_prefix = "GAPI"; - -// Monitoring configuration of the service. -// -// The example below shows how to configure monitored resources and metrics -// for monitoring. In the example, a monitored resource and two metrics are -// defined. The `library.googleapis.com/book/returned_count` metric is sent -// to both producer and consumer projects, whereas the -// `library.googleapis.com/book/overdue_count` metric is only sent to the -// consumer project. -// -// monitored_resources: -// - type: library.googleapis.com/branch -// labels: -// - key: /city -// description: The city where the library branch is located in. -// - key: /name -// description: The name of the branch. -// metrics: -// - name: library.googleapis.com/book/returned_count -// metric_kind: DELTA -// value_type: INT64 -// labels: -// - key: /customer_id -// - name: library.googleapis.com/book/overdue_count -// metric_kind: GAUGE -// value_type: INT64 -// labels: -// - key: /customer_id -// monitoring: -// producer_destinations: -// - monitored_resource: library.googleapis.com/branch -// metrics: -// - library.googleapis.com/book/returned_count -// consumer_destinations: -// - monitored_resource: library.googleapis.com/branch -// metrics: -// - library.googleapis.com/book/returned_count -// - library.googleapis.com/book/overdue_count -message Monitoring { - // Configuration of a specific monitoring destination (the producer project - // or the consumer project). - message MonitoringDestination { - // The monitored resource type. The type must be defined in - // [Service.monitored_resources][google.api.Service.monitored_resources] section. - string monitored_resource = 1; - - // Types of the metrics to report to this monitoring destination. - // Each type must be defined in [Service.metrics][google.api.Service.metrics] section. - repeated string metrics = 2; - } - - // Monitoring configurations for sending metrics to the producer project. - // There can be multiple producer destinations. A monitored resouce type may - // appear in multiple monitoring destinations if different aggregations are - // needed for different sets of metrics associated with that monitored - // resource type. A monitored resource and metric pair may only be used once - // in the Monitoring configuration. - repeated MonitoringDestination producer_destinations = 1; - - // Monitoring configurations for sending metrics to the consumer project. - // There can be multiple consumer destinations. A monitored resouce type may - // appear in multiple monitoring destinations if different aggregations are - // needed for different sets of metrics associated with that monitored - // resource type. A monitored resource and metric pair may only be used once - // in the Monitoring configuration. - repeated MonitoringDestination consumer_destinations = 2; -} diff --git a/third_party/google/api/quota.proto b/third_party/google/api/quota.proto deleted file mode 100644 index 2e6e52b66..000000000 --- a/third_party/google/api/quota.proto +++ /dev/null @@ -1,187 +0,0 @@ -// Copyright 2019 Google LLC. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -// - -syntax = "proto3"; - -package google.api; - -option go_package = "google.golang.org/genproto/googleapis/api/serviceconfig;serviceconfig"; -option java_multiple_files = true; -option java_outer_classname = "QuotaProto"; -option java_package = "com.google.api"; -option objc_class_prefix = "GAPI"; - -// Quota configuration helps to achieve fairness and budgeting in service -// usage. -// -// The metric based quota configuration works this way: -// - The service configuration defines a set of metrics. -// - For API calls, the quota.metric_rules maps methods to metrics with -// corresponding costs. -// - The quota.limits defines limits on the metrics, which will be used for -// quota checks at runtime. -// -// An example quota configuration in yaml format: -// -// quota: -// limits: -// -// - name: apiWriteQpsPerProject -// metric: library.googleapis.com/write_calls -// unit: "1/min/{project}" # rate limit for consumer projects -// values: -// STANDARD: 10000 -// -// -// # The metric rules bind all methods to the read_calls metric, -// # except for the UpdateBook and DeleteBook methods. These two methods -// # are mapped to the write_calls metric, with the UpdateBook method -// # consuming at twice rate as the DeleteBook method. -// metric_rules: -// - selector: "*" -// metric_costs: -// library.googleapis.com/read_calls: 1 -// - selector: google.example.library.v1.LibraryService.UpdateBook -// metric_costs: -// library.googleapis.com/write_calls: 2 -// - selector: google.example.library.v1.LibraryService.DeleteBook -// metric_costs: -// library.googleapis.com/write_calls: 1 -// -// Corresponding Metric definition: -// -// metrics: -// - name: library.googleapis.com/read_calls -// display_name: Read requests -// metric_kind: DELTA -// value_type: INT64 -// -// - name: library.googleapis.com/write_calls -// display_name: Write requests -// metric_kind: DELTA -// value_type: INT64 -// -// -message Quota { - // List of `QuotaLimit` definitions for the service. - repeated QuotaLimit limits = 3; - - // List of `MetricRule` definitions, each one mapping a selected method to one - // or more metrics. - repeated MetricRule metric_rules = 4; -} - -// Bind API methods to metrics. Binding a method to a metric causes that -// metric's configured quota behaviors to apply to the method call. -message MetricRule { - // Selects the methods to which this rule applies. - // - // Refer to [selector][google.api.DocumentationRule.selector] for syntax details. - string selector = 1; - - // Metrics to update when the selected methods are called, and the associated - // cost applied to each metric. - // - // The key of the map is the metric name, and the values are the amount - // increased for the metric against which the quota limits are defined. - // The value must not be negative. - map metric_costs = 2; -} - -// `QuotaLimit` defines a specific limit that applies over a specified duration -// for a limit type. There can be at most one limit for a duration and limit -// type combination defined within a `QuotaGroup`. -message QuotaLimit { - // Name of the quota limit. - // - // The name must be provided, and it must be unique within the service. The - // name can only include alphanumeric characters as well as '-'. - // - // The maximum length of the limit name is 64 characters. - string name = 6; - - // Optional. User-visible, extended description for this quota limit. - // Should be used only when more context is needed to understand this limit - // than provided by the limit's display name (see: `display_name`). - string description = 2; - - // Default number of tokens that can be consumed during the specified - // duration. This is the number of tokens assigned when a client - // application developer activates the service for his/her project. - // - // Specifying a value of 0 will block all requests. This can be used if you - // are provisioning quota to selected consumers and blocking others. - // Similarly, a value of -1 will indicate an unlimited quota. No other - // negative values are allowed. - // - // Used by group-based quotas only. - int64 default_limit = 3; - - // Maximum number of tokens that can be consumed during the specified - // duration. Client application developers can override the default limit up - // to this maximum. If specified, this value cannot be set to a value less - // than the default limit. If not specified, it is set to the default limit. - // - // To allow clients to apply overrides with no upper bound, set this to -1, - // indicating unlimited maximum quota. - // - // Used by group-based quotas only. - int64 max_limit = 4; - - // Free tier value displayed in the Developers Console for this limit. - // The free tier is the number of tokens that will be subtracted from the - // billed amount when billing is enabled. - // This field can only be set on a limit with duration "1d", in a billable - // group; it is invalid on any other limit. If this field is not set, it - // defaults to 0, indicating that there is no free tier for this service. - // - // Used by group-based quotas only. - int64 free_tier = 7; - - // Duration of this limit in textual notation. Example: "100s", "24h", "1d". - // For duration longer than a day, only multiple of days is supported. We - // support only "100s" and "1d" for now. Additional support will be added in - // the future. "0" indicates indefinite duration. - // - // Used by group-based quotas only. - string duration = 5; - - // The name of the metric this quota limit applies to. The quota limits with - // the same metric will be checked together during runtime. The metric must be - // defined within the service config. - string metric = 8; - - // Specify the unit of the quota limit. It uses the same syntax as - // [Metric.unit][]. The supported unit kinds are determined by the quota - // backend system. - // - // Here are some examples: - // * "1/min/{project}" for quota per minute per project. - // - // Note: the order of unit components is insignificant. - // The "1" at the beginning is required to follow the metric unit syntax. - string unit = 9; - - // Tiered limit values. You must specify this as a key:value pair, with an - // integer value that is the maximum number of requests allowed for the - // specified unit. Currently only STANDARD is supported. - map values = 10; - - // User-visible display name for this limit. - // Optional. If not set, the UI will provide a default display name based on - // the quota configuration. This field can be used to override the default - // display name generated from the configuration. - string display_name = 12; -} diff --git a/third_party/google/api/resource.proto b/third_party/google/api/resource.proto deleted file mode 100644 index 54f8aeb78..000000000 --- a/third_party/google/api/resource.proto +++ /dev/null @@ -1,199 +0,0 @@ -// Copyright 2019 Google LLC. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -// - -syntax = "proto3"; - -package google.api; - -import "google/protobuf/descriptor.proto"; - -option cc_enable_arenas = true; -option go_package = "google.golang.org/genproto/googleapis/api/annotations;annotations"; -option java_multiple_files = true; -option java_outer_classname = "ResourceProto"; -option java_package = "com.google.api"; -option objc_class_prefix = "GAPI"; - -extend google.protobuf.FieldOptions { - // The fully qualified message name of the type that this field references. - // Marks this as a field referring to a resource in another message. - // - // Example: - // - // message Subscription { - // string topic = 2 [(google.api.resource_reference) = { - // type: "pubsub.googleapis.com/Topic" - // }]; - // } - // - // If the referenced message is in the same proto package, the package - // may be omitted: - // - // message Subscription { - // string topic = 2 - // [(google.api.resource_reference).type = "Topic"]; - // } - // - // Only one of {`resource`, `resource_reference`} may be set. - google.api.ResourceReference resource_reference = 1055; -} - -extend google.protobuf.MessageOptions { - // An annotation describing a resource. - // - // Example: - // - // message Topic { - // option (google.api.resource) = { - // type: "pubsub.googleapis.com/Topic" - // pattern: "projects/{project}/topics/{topic}" - // }; - // } - // - // Only one of {`resource`, `resource_reference`} may be set. - google.api.ResourceDescriptor resource = 1053; -} - -// A simple descriptor of a resource type. -// -// ResourceDescriptor annotates a resource message (either by means of a -// protobuf annotation or use in the service config), and associates the -// resource's schema, the resource type, and the pattern of the resource name. -// -// Example: -// -// message Topic { -// // Indicates this message defines a resource schema. -// // Declares the resource type in the format of {service}/{kind}. -// // For Kubernetes resources, the format is {api group}/{kind}. -// option (google.api.resource) = { -// type: "pubsub.googleapis.com/Topic" -// pattern: "projects/{project}/topics/{topic}" -// }; -// } -// -// Sometimes, resources have multiple patterns, typically because they can -// live under multiple parents. -// -// Example: -// -// message LogEntry { -// option (google.api.resource) = { -// type: "logging.googleapis.com/LogEntry" -// pattern: "projects/{project}/logs/{log}" -// pattern: "organizations/{organization}/logs/{log}" -// pattern: "folders/{folder}/logs/{log}" -// pattern: "billingAccounts/{billing_account}/logs/{log}" -// }; -// } -message ResourceDescriptor { - // A description of the historical or future-looking state of the - // resource pattern. - enum History { - // The "unset" value. - HISTORY_UNSPECIFIED = 0; - - // The resource originally had one pattern and launched as such, and - // additional patterns were added later. - ORIGINALLY_SINGLE_PATTERN = 1; - - // The resource has one pattern, but the API owner expects to add more - // later. (This is the inverse of ORIGINALLY_SINGLE_PATTERN, and prevents - // that from being necessary once there are multiple patterns.) - FUTURE_MULTI_PATTERN = 2; - } - - // The full name of the resource type. It must be in the format of - // {service_name}/{resource_type_kind}. The resource type names are - // singular and do not contain version numbers. - // - // For example: `storage.googleapis.com/Bucket` - // - // The value of the resource_type_kind must follow the regular expression - // /[A-Z][a-zA-Z0-9]+/. It must start with upper case character and - // recommended to use PascalCase (UpperCamelCase). The maximum number of - // characters allowed for the resource_type_kind is 100. - string type = 1; - - // Required. The valid pattern or patterns for this resource's names. - // - // Examples: - // - "projects/{project}/topics/{topic}" - // - "projects/{project}/knowledgeBases/{knowledge_base}" - // - // The components in braces correspond to the IDs for each resource in the - // hierarchy. It is expected that, if multiple patterns are provided, - // the same component name (e.g. "project") refers to IDs of the same - // type of resource. - repeated string pattern = 2; - - // Optional. The field on the resource that designates the resource name - // field. If omitted, this is assumed to be "name". - string name_field = 3; - - // Optional. The historical or future-looking state of the resource pattern. - // - // Example: - // // The InspectTemplate message originally only supported resource - // // names with organization, and project was added later. - // message InspectTemplate { - // option (google.api.resource) = { - // type: "dlp.googleapis.com/InspectTemplate" - // pattern: "organizations/{organization}/inspectTemplates/{inspect_template}" - // pattern: "projects/{project}/inspectTemplates/{inspect_template}" - // history: ORIGINALLY_SINGLE_PATTERN - // }; - // } - History history = 4; -} - -// An annotation designating that this field is a reference to a resource -// defined by another message. -message ResourceReference { - // The unified resource type name of the type that this field references. - // Marks this as a field referring to a resource in another message. - // - // Example: - // - // message Subscription { - // string topic = 2 [(google.api.resource_reference) = { - // type = "pubsub.googleapis.com/Topic" - // }]; - // } - string type = 1; - - // The fully-qualified message name of a child of the type that this field - // references. - // - // This is useful for `parent` fields where a resource has more than one - // possible type of parent. - // - // Example: - // - // message ListLogEntriesRequest { - // string parent = 1 [(google.api.resource_reference) = { - // child_type: "logging.googleapis.com/LogEntry" - // }; - // } - // - // If the referenced message is in the same proto package, the service name - // may be omitted: - // - // message ListLogEntriesRequest { - // string parent = 1 - // [(google.api.resource_reference).child_type = "LogEntry"]; - // } - string child_type = 2; -} diff --git a/third_party/google/api/service.proto b/third_party/google/api/service.proto deleted file mode 100644 index 33b69682f..000000000 --- a/third_party/google/api/service.proto +++ /dev/null @@ -1,180 +0,0 @@ -// Copyright 2019 Google LLC. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -// - -syntax = "proto3"; - -package google.api; - -import "google/api/annotations.proto"; -import "google/api/auth.proto"; -import "google/api/backend.proto"; -import "google/api/billing.proto"; -import "google/api/context.proto"; -import "google/api/control.proto"; -import "google/api/documentation.proto"; -import "google/api/endpoint.proto"; -import "google/api/experimental/experimental.proto"; -import "google/api/http.proto"; -import "google/api/label.proto"; -import "google/api/log.proto"; -import "google/api/logging.proto"; -import "google/api/metric.proto"; -import "google/api/monitored_resource.proto"; -import "google/api/monitoring.proto"; -import "google/api/quota.proto"; -import "google/api/source_info.proto"; -import "google/api/system_parameter.proto"; -import "google/api/usage.proto"; -import "google/protobuf/any.proto"; -import "google/protobuf/api.proto"; -import "google/protobuf/type.proto"; -import "google/protobuf/wrappers.proto"; - -option go_package = "google.golang.org/genproto/googleapis/api/serviceconfig;serviceconfig"; -option java_multiple_files = true; -option java_outer_classname = "ServiceProto"; -option java_package = "com.google.api"; -option objc_class_prefix = "GAPI"; - -// `Service` is the root object of Google service configuration schema. It -// describes basic information about a service, such as the name and the -// title, and delegates other aspects to sub-sections. Each sub-section is -// either a proto message or a repeated proto message that configures a -// specific aspect, such as auth. See each proto message definition for details. -// -// Example: -// -// type: google.api.Service -// config_version: 3 -// name: calendar.googleapis.com -// title: Google Calendar API -// apis: -// - name: google.calendar.v3.Calendar -// authentication: -// providers: -// - id: google_calendar_auth -// jwks_uri: https://www.googleapis.com/oauth2/v1/certs -// issuer: https://securetoken.google.com -// rules: -// - selector: "*" -// requirements: -// provider_id: google_calendar_auth -message Service { - // The semantic version of the service configuration. The config version - // affects the interpretation of the service configuration. For example, - // certain features are enabled by default for certain config versions. - // The latest config version is `3`. - google.protobuf.UInt32Value config_version = 20; - - // The service name, which is a DNS-like logical identifier for the - // service, such as `calendar.googleapis.com`. The service name - // typically goes through DNS verification to make sure the owner - // of the service also owns the DNS name. - string name = 1; - - // A unique ID for a specific instance of this message, typically assigned - // by the client for tracking purpose. If empty, the server may choose to - // generate one instead. Must be no longer than 60 characters. - string id = 33; - - // The product title for this service. - string title = 2; - - // The Google project that owns this service. - string producer_project_id = 22; - - // A list of API interfaces exported by this service. Only the `name` field - // of the [google.protobuf.Api][google.protobuf.Api] needs to be provided by the configuration - // author, as the remaining fields will be derived from the IDL during the - // normalization process. It is an error to specify an API interface here - // which cannot be resolved against the associated IDL files. - repeated google.protobuf.Api apis = 3; - - // A list of all proto message types included in this API service. - // Types referenced directly or indirectly by the `apis` are - // automatically included. Messages which are not referenced but - // shall be included, such as types used by the `google.protobuf.Any` type, - // should be listed here by name. Example: - // - // types: - // - name: google.protobuf.Int32 - repeated google.protobuf.Type types = 4; - - // A list of all enum types included in this API service. Enums - // referenced directly or indirectly by the `apis` are automatically - // included. Enums which are not referenced but shall be included - // should be listed here by name. Example: - // - // enums: - // - name: google.someapi.v1.SomeEnum - repeated google.protobuf.Enum enums = 5; - - // Additional API documentation. - Documentation documentation = 6; - - // API backend configuration. - Backend backend = 8; - - // HTTP configuration. - Http http = 9; - - // Quota configuration. - Quota quota = 10; - - // Auth configuration. - Authentication authentication = 11; - - // Context configuration. - Context context = 12; - - // Configuration controlling usage of this service. - Usage usage = 15; - - // Configuration for network endpoints. If this is empty, then an endpoint - // with the same name as the service is automatically generated to service all - // defined APIs. - repeated Endpoint endpoints = 18; - - // Configuration for the service control plane. - Control control = 21; - - // Defines the logs used by this service. - repeated LogDescriptor logs = 23; - - // Defines the metrics used by this service. - repeated MetricDescriptor metrics = 24; - - // Defines the monitored resources used by this service. This is required - // by the [Service.monitoring][google.api.Service.monitoring] and [Service.logging][google.api.Service.logging] configurations. - repeated MonitoredResourceDescriptor monitored_resources = 25; - - // Billing configuration. - Billing billing = 26; - - // Logging configuration. - Logging logging = 27; - - // Monitoring configuration. - Monitoring monitoring = 28; - - // System parameter configuration. - SystemParameters system_parameters = 29; - - // Output only. The source information for this configuration if available. - SourceInfo source_info = 37; - - // Experimental configuration. - Experimental experimental = 101; -} diff --git a/third_party/google/api/serviceconfig.yaml b/third_party/google/api/serviceconfig.yaml deleted file mode 100644 index 6d883d428..000000000 --- a/third_party/google/api/serviceconfig.yaml +++ /dev/null @@ -1,24 +0,0 @@ -type: google.api.Service -config_version: 1 -name: serviceconfig.googleapis.com -title: Service Config API - -types: -- name: google.api.ConfigChange -- name: google.api.Distribution -- name: google.api.DocumentationRule -- name: google.api.HttpBody -- name: google.api.LabelDescriptor -- name: google.api.Metric -- name: google.api.MonitoredResource -- name: google.api.MonitoredResourceDescriptor -- name: google.api.MonitoredResourceMetadata -- name: google.api.ResourceDescriptor -- name: google.api.ResourceReference -- name: google.api.Service - -enums: -- name: google.api.FieldBehavior - -documentation: - summary: Lets you define and config your API service. diff --git a/third_party/google/api/servicecontrol/README.md b/third_party/google/api/servicecontrol/README.md deleted file mode 100644 index 3d9590ee0..000000000 --- a/third_party/google/api/servicecontrol/README.md +++ /dev/null @@ -1,126 +0,0 @@ -Google Service Control provides control plane functionality to managed services, -such as logging, monitoring, and status checks. This page provides an overview -of what it does and how it works. - -## Why use Service Control? - -When you develop a cloud service, you typically start with the business -requirements and the architecture design, then proceed with API definition -and implementation. Before you put your service into production, you -need to deal with many control plane issues: - -* How to control access to your service. -* How to send logging and monitoring data to both consumers and producers. -* How to create and manage dashboards to visualize this data. -* How to automatically scale the control plane components with your service. - -Service Control is a mature and feature-rich control plane provider -that addresses these needs with high efficiency, high scalability, -and high availability. It provides a simple public API that can be accessed -from anywhere using JSON REST and gRPC clients, so when you move your service -from on-premise to a cloud provider, or from one cloud provider to another, -you don't need to change the control plane provider. - -Services built using Google Cloud Endpoints already take advantage of -Service Control. Cloud Endpoints sends logging and monitoring data -through Google Service Control for every request arriving at its -proxy. If you need to report any additional logging and monitoring data for -your Cloud Endpoints service, you can call the Service Control API directly -from your service. - -The Service Control API definition is open sourced and available on -[GitHub](https://github.com/googleapis/googleapis/tree/master/google/api/servicecontrol). -By changing the DNS name, you can easily use alternative implementations of -the Service Control API. - -## Architecture - -Google Service Control works with a set of *managed services* and their -*operations* (activities), *checks* whether an operation is allowed to proceed, -and *reports* completed operations. Behind the scenes, it leverages other -Google Cloud services, such as -[Google Service Management](/service-management), -[Stackdriver Logging](/logging), and [Stackdriver Monitoring](/monitoring), -while hiding their complexity from service producers. It enables service -producers to send telemetry data to their consumers. It uses caching, -batching, aggregation, and retries to deliver higher performance and -availability than the individual backend systems it encapsulates. - -
-
- The overall architecture of a service that uses Google Service Control. -
-
Figure 1: Using Google Service Control.
-
- -The Service Control API provides two methods: - -* [`services.check`](/service-control/reference/rest/v1/services/check), used for: - * Ensuring valid consumer status - * Validating API keys -* [`services.report`](/service-control/reference/rest/v1/services/report), used for: - * Sending logs to Stackdriver Logging - * Sending metrics to Stackdriver Monitoring - -We’ll look at these in more detail in the rest of this overview. - -## Managed services - -A [managed service](/service-management/reference/rest/v1/services) is -a network service managed by -[Google Service Management](/service-management). Each managed service has a -unique name, such as `example.googleapis.com`, which must be a valid -fully-qualified DNS name, as per RFC 1035. - -For example: - -* Google Cloud Pub/Sub (`pubsub.googleapis.com`) -* Google Cloud Vision (`vision.googleapis.com`) -* Google Cloud Bigtable (`bigtable.googleapis.com`) -* Google Cloud Datastore (`datastore.googleapis.com`) - -Google Service Management manages the lifecycle of each service’s -configuration, which is used to customize Google Service Control's behavior. -Service configurations are also used by Google Cloud Console -for displaying APIs and their settings, enabling/disabling APIs, and more. - -## Operations - -Google Service Control uses the generic concept of an *operation* -to represent the -activities of a managed service, such as API calls and resource usage. Each -operation is associated with a managed service and a specific service -consumer, and has a set of properties that describe the operation, such as -the API method name and resource usage amount. For more information, see the -[Operation definition](/service-control/rest/v1/Operation). - -## Check - -The [`services.check`](/service-control/reference/rest/v1/services/check) -method determines whether an operation should be allowed to proceed -for a managed service. - -For example: - -* Check if the consumer is still active. -* Check if the consumer has enabled the service. -* Check if the API key is still valid. - -By performing multiple checks within a single method call, it provides -better performance, higher reliability, and reduced development cost to -service producers compared to checking with multiple backend systems. - -## Report - -The [`services.report`](/service-control/reference/rest/v1/services/report) -method reports completed operations for -a managed service to backend systems, such as logging and monitoring. The -reported data can be seen in Google API Console and Google Cloud Console, -and retrieved with appropriate APIs, such as the Stackdriver Logging and -Stackdriver Monitoring APIs. - -## Next steps - -* Read our [Getting Started guide](/service-control/getting-started) to find out - how to set up and use the Google Service Control API. diff --git a/third_party/google/api/servicecontrol/v1/check_error.proto b/third_party/google/api/servicecontrol/v1/check_error.proto deleted file mode 100644 index 3395839d8..000000000 --- a/third_party/google/api/servicecontrol/v1/check_error.proto +++ /dev/null @@ -1,98 +0,0 @@ -// Copyright 2017 Google Inc. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -syntax = "proto3"; - -package google.api.servicecontrol.v1; - -import "google/api/annotations.proto"; - -option cc_enable_arenas = true; -option go_package = "google.golang.org/genproto/googleapis/api/servicecontrol/v1;servicecontrol"; -option java_multiple_files = true; -option java_outer_classname = "CheckErrorProto"; -option java_package = "com.google.api.servicecontrol.v1"; - -// Defines the errors to be returned in -// [google.api.servicecontrol.v1.CheckResponse.check_errors][google.api.servicecontrol.v1.CheckResponse.check_errors]. -message CheckError { - // Error codes for Check responses. - enum Code { - // This is never used in `CheckResponse`. - ERROR_CODE_UNSPECIFIED = 0; - - // The consumer's project id was not found. - // Same as [google.rpc.Code.NOT_FOUND][]. - NOT_FOUND = 5; - - // The consumer doesn't have access to the specified resource. - // Same as [google.rpc.Code.PERMISSION_DENIED][]. - PERMISSION_DENIED = 7; - - // Quota check failed. Same as [google.rpc.Code.RESOURCE_EXHAUSTED][]. - RESOURCE_EXHAUSTED = 8; - - // The consumer hasn't activated the service. - SERVICE_NOT_ACTIVATED = 104; - - // The consumer cannot access the service because billing is disabled. - BILLING_DISABLED = 107; - - // The consumer's project has been marked as deleted (soft deletion). - PROJECT_DELETED = 108; - - // The consumer's project number or id does not represent a valid project. - PROJECT_INVALID = 114; - - // The IP address of the consumer is invalid for the specific consumer - // project. - IP_ADDRESS_BLOCKED = 109; - - // The referer address of the consumer request is invalid for the specific - // consumer project. - REFERER_BLOCKED = 110; - - // The client application of the consumer request is invalid for the - // specific consumer project. - CLIENT_APP_BLOCKED = 111; - - // The API targeted by this request is invalid for the specified consumer - // project. - API_TARGET_BLOCKED = 122; - - // The consumer's API key is invalid. - API_KEY_INVALID = 105; - - // The consumer's API Key has expired. - API_KEY_EXPIRED = 112; - - // The consumer's API Key was not found in config record. - API_KEY_NOT_FOUND = 113; - - // The backend server for looking up project id/number is unavailable. - NAMESPACE_LOOKUP_UNAVAILABLE = 300; - - // The backend server for checking service status is unavailable. - SERVICE_STATUS_UNAVAILABLE = 301; - - // The backend server for checking billing status is unavailable. - BILLING_STATUS_UNAVAILABLE = 302; - } - - // The error code. - Code code = 1; - - // Free-form text providing details on the error cause of the error. - string detail = 2; -} diff --git a/third_party/google/api/servicecontrol/v1/distribution.proto b/third_party/google/api/servicecontrol/v1/distribution.proto deleted file mode 100644 index 40b89f5ba..000000000 --- a/third_party/google/api/servicecontrol/v1/distribution.proto +++ /dev/null @@ -1,158 +0,0 @@ -// Copyright 2017 Google Inc. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -syntax = "proto3"; - -package google.api.servicecontrol.v1; - -option cc_enable_arenas = true; -option go_package = "google.golang.org/genproto/googleapis/api/servicecontrol/v1;servicecontrol"; -option java_multiple_files = true; -option java_outer_classname = "DistributionProto"; -option java_package = "com.google.api.servicecontrol.v1"; - -// Distribution represents a frequency distribution of double-valued sample -// points. It contains the size of the population of sample points plus -// additional optional information: -// -// - the arithmetic mean of the samples -// - the minimum and maximum of the samples -// - the sum-squared-deviation of the samples, used to compute variance -// - a histogram of the values of the sample points -message Distribution { - // Describing buckets with constant width. - message LinearBuckets { - // The number of finite buckets. With the underflow and overflow buckets, - // the total number of buckets is `num_finite_buckets` + 2. - // See comments on `bucket_options` for details. - int32 num_finite_buckets = 1; - - // The i'th linear bucket covers the interval - // [offset + (i-1) * width, offset + i * width) - // where i ranges from 1 to num_finite_buckets, inclusive. - // Must be strictly positive. - double width = 2; - - // The i'th linear bucket covers the interval - // [offset + (i-1) * width, offset + i * width) - // where i ranges from 1 to num_finite_buckets, inclusive. - double offset = 3; - } - - // Describing buckets with exponentially growing width. - message ExponentialBuckets { - // The number of finite buckets. With the underflow and overflow buckets, - // the total number of buckets is `num_finite_buckets` + 2. - // See comments on `bucket_options` for details. - int32 num_finite_buckets = 1; - - // The i'th exponential bucket covers the interval - // [scale * growth_factor^(i-1), scale * growth_factor^i) - // where i ranges from 1 to num_finite_buckets inclusive. - // Must be larger than 1.0. - double growth_factor = 2; - - // The i'th exponential bucket covers the interval - // [scale * growth_factor^(i-1), scale * growth_factor^i) - // where i ranges from 1 to num_finite_buckets inclusive. - // Must be > 0. - double scale = 3; - } - - // Describing buckets with arbitrary user-provided width. - message ExplicitBuckets { - // 'bound' is a list of strictly increasing boundaries between - // buckets. Note that a list of length N-1 defines N buckets because - // of fenceposting. See comments on `bucket_options` for details. - // - // The i'th finite bucket covers the interval - // [bound[i-1], bound[i]) - // where i ranges from 1 to bound_size() - 1. Note that there are no - // finite buckets at all if 'bound' only contains a single element; in - // that special case the single bound defines the boundary between the - // underflow and overflow buckets. - // - // bucket number lower bound upper bound - // i == 0 (underflow) -inf bound[i] - // 0 < i < bound_size() bound[i-1] bound[i] - // i == bound_size() (overflow) bound[i-1] +inf - repeated double bounds = 1; - } - - // The total number of samples in the distribution. Must be >= 0. - int64 count = 1; - - // The arithmetic mean of the samples in the distribution. If `count` is - // zero then this field must be zero. - double mean = 2; - - // The minimum of the population of values. Ignored if `count` is zero. - double minimum = 3; - - // The maximum of the population of values. Ignored if `count` is zero. - double maximum = 4; - - // The sum of squared deviations from the mean: - // Sum[i=1..count]((x_i - mean)^2) - // where each x_i is a sample values. If `count` is zero then this field - // must be zero, otherwise validation of the request fails. - double sum_of_squared_deviation = 5; - - // The number of samples in each histogram bucket. `bucket_counts` are - // optional. If present, they must sum to the `count` value. - // - // The buckets are defined below in `bucket_option`. There are N buckets. - // `bucket_counts[0]` is the number of samples in the underflow bucket. - // `bucket_counts[1]` to `bucket_counts[N-1]` are the numbers of samples - // in each of the finite buckets. And `bucket_counts[N] is the number - // of samples in the overflow bucket. See the comments of `bucket_option` - // below for more details. - // - // Any suffix of trailing zeros may be omitted. - repeated int64 bucket_counts = 6; - - // Defines the buckets in the histogram. `bucket_option` and `bucket_counts` - // must be both set, or both unset. - // - // Buckets are numbered in the range of [0, N], with a total of N+1 buckets. - // There must be at least two buckets (a single-bucket histogram gives - // no information that isn't already provided by `count`). - // - // The first bucket is the underflow bucket which has a lower bound - // of -inf. The last bucket is the overflow bucket which has an - // upper bound of +inf. All other buckets (if any) are called "finite" - // buckets because they have finite lower and upper bounds. As described - // below, there are three ways to define the finite buckets. - // - // (1) Buckets with constant width. - // (2) Buckets with exponentially growing widths. - // (3) Buckets with arbitrary user-provided widths. - // - // In all cases, the buckets cover the entire real number line (-inf, - // +inf). Bucket upper bounds are exclusive and lower bounds are - // inclusive. The upper bound of the underflow bucket is equal to the - // lower bound of the smallest finite bucket; the lower bound of the - // overflow bucket is equal to the upper bound of the largest finite - // bucket. - oneof bucket_option { - // Buckets with constant width. - LinearBuckets linear_buckets = 7; - - // Buckets with exponentially growing width. - ExponentialBuckets exponential_buckets = 8; - - // Buckets with arbitrary user-provided width. - ExplicitBuckets explicit_buckets = 9; - } -} diff --git a/third_party/google/api/servicecontrol/v1/log_entry.proto b/third_party/google/api/servicecontrol/v1/log_entry.proto deleted file mode 100644 index 50b0fc468..000000000 --- a/third_party/google/api/servicecontrol/v1/log_entry.proto +++ /dev/null @@ -1,66 +0,0 @@ -// Copyright 2017 Google Inc. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -syntax = "proto3"; - -package google.api.servicecontrol.v1; - -import "google/api/annotations.proto"; -import "google/logging/type/log_severity.proto"; -import "google/protobuf/any.proto"; -import "google/protobuf/struct.proto"; -import "google/protobuf/timestamp.proto"; - -option go_package = "google.golang.org/genproto/googleapis/api/servicecontrol/v1;servicecontrol"; -option java_multiple_files = true; -option java_outer_classname = "LogEntryProto"; -option java_package = "com.google.api.servicecontrol.v1"; - -// An individual log entry. -message LogEntry { - // Required. The log to which this log entry belongs. Examples: `"syslog"`, - // `"book_log"`. - string name = 10; - - // The time the event described by the log entry occurred. If - // omitted, defaults to operation start time. - google.protobuf.Timestamp timestamp = 11; - - // The severity of the log entry. The default value is - // `LogSeverity.DEFAULT`. - google.logging.type.LogSeverity severity = 12; - - // A unique ID for the log entry used for deduplication. If omitted, - // the implementation will generate one based on operation_id. - string insert_id = 4; - - // A set of user-defined (key, value) data that provides additional - // information about the log entry. - map labels = 13; - - // The log entry payload, which can be one of multiple types. - oneof payload { - // The log entry payload, represented as a protocol buffer that is - // expressed as a JSON object. The only accepted type currently is - // [AuditLog][google.cloud.audit.AuditLog]. - google.protobuf.Any proto_payload = 2; - - // The log entry payload, represented as a Unicode string (UTF-8). - string text_payload = 3; - - // The log entry payload, represented as a structure that - // is expressed as a JSON object. - google.protobuf.Struct struct_payload = 6; - } -} diff --git a/third_party/google/api/servicecontrol/v1/metric_value.proto b/third_party/google/api/servicecontrol/v1/metric_value.proto deleted file mode 100644 index 9a62ff698..000000000 --- a/third_party/google/api/servicecontrol/v1/metric_value.proto +++ /dev/null @@ -1,78 +0,0 @@ -// Copyright 2017 Google Inc. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -syntax = "proto3"; - -package google.api.servicecontrol.v1; - -import "google/api/annotations.proto"; -import "google/api/servicecontrol/v1/distribution.proto"; -import "google/protobuf/timestamp.proto"; -import "google/type/money.proto"; - -option cc_enable_arenas = true; -option go_package = "google.golang.org/genproto/googleapis/api/servicecontrol/v1;servicecontrol"; -option java_multiple_files = true; -option java_outer_classname = "MetricValueSetProto"; -option java_package = "com.google.api.servicecontrol.v1"; - -// Represents a single metric value. -message MetricValue { - // The labels describing the metric value. - // See comments on - // [google.api.servicecontrol.v1.Operation.labels][google.api.servicecontrol.v1.Operation.labels] - // for the overriding relationship. - map labels = 1; - - // The start of the time period over which this metric value's measurement - // applies. The time period has different semantics for different metric - // types (cumulative, delta, and gauge). See the metric definition - // documentation in the service configuration for details. - google.protobuf.Timestamp start_time = 2; - - // The end of the time period over which this metric value's measurement - // applies. - google.protobuf.Timestamp end_time = 3; - - // The value. The type of value used in the request must - // agree with the metric definition in the service configuration, otherwise - // the MetricValue is rejected. - oneof value { - // A boolean value. - bool bool_value = 4; - - // A signed 64-bit integer value. - int64 int64_value = 5; - - // A double precision floating point value. - double double_value = 6; - - // A text string value. - string string_value = 7; - - // A distribution value. - Distribution distribution_value = 8; - } -} - -// Represents a set of metric values in the same metric. -// Each metric value in the set should have a unique combination of start time, -// end time, and label values. -message MetricValueSet { - // The metric name defined in the service configuration. - string metric_name = 1; - - // The values in this metric. - repeated MetricValue metric_values = 2; -} diff --git a/third_party/google/api/servicecontrol/v1/operation.proto b/third_party/google/api/servicecontrol/v1/operation.proto deleted file mode 100644 index 301f3575c..000000000 --- a/third_party/google/api/servicecontrol/v1/operation.proto +++ /dev/null @@ -1,113 +0,0 @@ -// Copyright 2017 Google Inc. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -syntax = "proto3"; - -package google.api.servicecontrol.v1; - -import "google/api/annotations.proto"; -import "google/api/servicecontrol/v1/log_entry.proto"; -import "google/api/servicecontrol/v1/metric_value.proto"; -import "google/protobuf/timestamp.proto"; - -option cc_enable_arenas = true; -option go_package = "google.golang.org/genproto/googleapis/api/servicecontrol/v1;servicecontrol"; -option java_multiple_files = true; -option java_outer_classname = "OperationProto"; -option java_package = "com.google.api.servicecontrol.v1"; - -// Represents information regarding an operation. -message Operation { - // Defines the importance of the data contained in the operation. - enum Importance { - // The API implementation may cache and aggregate the data. - // The data may be lost when rare and unexpected system failures occur. - LOW = 0; - - // The API implementation doesn't cache and aggregate the data. - // If the method returns successfully, it's guaranteed that the data has - // been persisted in durable storage. - HIGH = 1; - } - - // Identity of the operation. This must be unique within the scope of the - // service that generated the operation. If the service calls - // Check() and Report() on the same operation, the two calls should carry - // the same id. - // - // UUID version 4 is recommended, though not required. - // In scenarios where an operation is computed from existing information - // and an idempotent id is desirable for deduplication purpose, UUID version 5 - // is recommended. See RFC 4122 for details. - string operation_id = 1; - - // Fully qualified name of the operation. Reserved for future use. - string operation_name = 2; - - // Identity of the consumer who is using the service. - // This field should be filled in for the operations initiated by a - // consumer, but not for service-initiated operations that are - // not related to a specific consumer. - // - // This can be in one of the following formats: - // project:, - // project_number:, - // api_key:. - string consumer_id = 3; - - // Required. Start time of the operation. - google.protobuf.Timestamp start_time = 4; - - // End time of the operation. - // Required when the operation is used in - // [ServiceController.Report][google.api.servicecontrol.v1.ServiceController.Report], - // but optional when the operation is used in - // [ServiceController.Check][google.api.servicecontrol.v1.ServiceController.Check]. - google.protobuf.Timestamp end_time = 5; - - // Labels describing the operation. Only the following labels are allowed: - // - // - Labels describing monitored resources as defined in - // the service configuration. - // - Default labels of metric values. When specified, labels defined in the - // metric value override these default. - // - The following labels defined by Google Cloud Platform: - // - `cloud.googleapis.com/location` describing the location where the - // operation happened, - // - `servicecontrol.googleapis.com/user_agent` describing the user agent - // of the API request, - // - `servicecontrol.googleapis.com/service_agent` describing the service - // used to handle the API request (e.g. ESP), - // - `servicecontrol.googleapis.com/platform` describing the platform - // where the API is served (e.g. GAE, GCE, GKE). - map labels = 6; - - // Represents information about this operation. Each MetricValueSet - // corresponds to a metric defined in the service configuration. - // The data type used in the MetricValueSet must agree with - // the data type specified in the metric definition. - // - // Within a single operation, it is not allowed to have more than one - // MetricValue instances that have the same metric names and identical - // label value combinations. If a request has such duplicated MetricValue - // instances, the entire request is rejected with - // an invalid argument error. - repeated MetricValueSet metric_value_sets = 7; - - // Represents information to be logged. - repeated LogEntry log_entries = 8; - - // DO NOT USE. This is an experimental field. - Importance importance = 11; -} diff --git a/third_party/google/api/servicecontrol/v1/quota_controller.proto b/third_party/google/api/servicecontrol/v1/quota_controller.proto deleted file mode 100644 index 808a73545..000000000 --- a/third_party/google/api/servicecontrol/v1/quota_controller.proto +++ /dev/null @@ -1,206 +0,0 @@ -// Copyright 2017 Google Inc. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -syntax = "proto3"; - -package google.api.servicecontrol.v1; - -import "google/api/annotations.proto"; -import "google/api/servicecontrol/v1/metric_value.proto"; - -option cc_enable_arenas = true; -option go_package = "google.golang.org/genproto/googleapis/api/servicecontrol/v1;servicecontrol"; -option java_multiple_files = true; -option java_outer_classname = "QuotaControllerProto"; -option java_package = "com.google.api.servicecontrol.v1"; - -// [Google Quota Control API](/service-control/overview) -// -// Allows clients to allocate and release quota against a [managed -// service](https://cloud.google.com/service-management/reference/rpc/google.api/servicemanagement.v1#google.api.servicemanagement.v1.ManagedService). -service QuotaController { - // Attempts to allocate quota for the specified consumer. It should be called - // before the operation is executed. - // - // This method requires the `servicemanagement.services.quota` - // permission on the specified service. For more information, see - // [Cloud IAM](https://cloud.google.com/iam). - // - // **NOTE:** The client **must** fail-open on server errors `INTERNAL`, - // `UNKNOWN`, `DEADLINE_EXCEEDED`, and `UNAVAILABLE`. To ensure system - // reliability, the server may inject these errors to prohibit any hard - // dependency on the quota functionality. - rpc AllocateQuota(AllocateQuotaRequest) returns (AllocateQuotaResponse) { - option (google.api.http) = { - post: "/v1/services/{service_name}:allocateQuota" - body: "*" - }; - } -} - -// Request message for the AllocateQuota method. -message AllocateQuotaRequest { - // Name of the service as specified in the service configuration. For example, - // `"pubsub.googleapis.com"`. - // - // See [google.api.Service][google.api.Service] for the definition of a - // service name. - string service_name = 1; - - // Operation that describes the quota allocation. - QuotaOperation allocate_operation = 2; - - // Specifies which version of service configuration should be used to process - // the request. If unspecified or no matching version can be found, the latest - // one will be used. - string service_config_id = 4; -} - -// Represents information regarding a quota operation. -message QuotaOperation { - // Supported quota modes. - enum QuotaMode { - // Guard against implicit default. Must not be used. - UNSPECIFIED = 0; - - // For AllocateQuota request, allocates quota for the amount specified in - // the service configuration or specified using the quota metrics. If the - // amount is higher than the available quota, allocation error will be - // returned and no quota will be allocated. - NORMAL = 1; - - // The operation allocates quota for the amount specified in the service - // configuration or specified using the quota metrics. If the amount is - // higher than the available quota, request does not fail but all available - // quota will be allocated. - BEST_EFFORT = 2; - - // For AllocateQuota request, only checks if there is enough quota - // available and does not change the available quota. No lock is placed on - // the available quota either. - CHECK_ONLY = 3; - } - - // Identity of the operation. This is expected to be unique within the scope - // of the service that generated the operation, and guarantees idempotency in - // case of retries. - // - // UUID version 4 is recommended, though not required. In scenarios where an - // operation is computed from existing information and an idempotent id is - // desirable for deduplication purpose, UUID version 5 is recommended. See - // RFC 4122 for details. - string operation_id = 1; - - // Fully qualified name of the API method for which this quota operation is - // requested. This name is used for matching quota rules or metric rules and - // billing status rules defined in service configuration. This field is not - // required if the quota operation is performed on non-API resources. - // - // Example of an RPC method name: - // google.example.library.v1.LibraryService.CreateShelf - string method_name = 2; - - // Identity of the consumer for whom this quota operation is being performed. - // - // This can be in one of the following formats: - // project:, - // project_number:, - // api_key:. - string consumer_id = 3; - - // Labels describing the operation. - map labels = 4; - - // Represents information about this operation. Each MetricValueSet - // corresponds to a metric defined in the service configuration. - // The data type used in the MetricValueSet must agree with - // the data type specified in the metric definition. - // - // Within a single operation, it is not allowed to have more than one - // MetricValue instances that have the same metric names and identical - // label value combinations. If a request has such duplicated MetricValue - // instances, the entire request is rejected with - // an invalid argument error. - repeated MetricValueSet quota_metrics = 5; - - // Quota mode for this operation. - QuotaMode quota_mode = 6; -} - -// Response message for the AllocateQuota method. -message AllocateQuotaResponse { - // The same operation_id value used in the AllocateQuotaRequest. Used for - // logging and diagnostics purposes. - string operation_id = 1; - - // Indicates the decision of the allocate. - repeated QuotaError allocate_errors = 2; - - // Quota metrics to indicate the result of allocation. Depending on the - // request, one or more of the following metrics will be included: - // - // 1. Per quota group or per quota metric incremental usage will be specified - // using the following delta metric : - // "serviceruntime.googleapis.com/api/consumer/quota_used_count" - // - // 2. The quota limit reached condition will be specified using the following - // boolean metric : - // "serviceruntime.googleapis.com/quota/exceeded" - repeated MetricValueSet quota_metrics = 3; - - // ID of the actual config used to process the request. - string service_config_id = 4; -} - -// Represents error information for -// [QuotaOperation][google.api.servicecontrol.v1.QuotaOperation]. -message QuotaError { - // Error codes related to project config validations are deprecated since the - // quota controller methods do not perform these validations. Instead services - // have to call the Check method, without quota_properties field, to perform - // these validations before calling the quota controller methods. These - // methods check only for project deletion to be wipe out compliant. - enum Code { - // This is never used. - UNSPECIFIED = 0; - - // Quota allocation failed. - // Same as [google.rpc.Code.RESOURCE_EXHAUSTED][]. - RESOURCE_EXHAUSTED = 8; - - // Consumer cannot access the service because the service requires active - // billing. - BILLING_NOT_ACTIVE = 107; - - // Consumer's project has been marked as deleted (soft deletion). - PROJECT_DELETED = 108; - - // Specified API key is invalid. - API_KEY_INVALID = 105; - - // Specified API Key has expired. - API_KEY_EXPIRED = 112; - } - - // Error code. - Code code = 1; - - // Subject to whom this error applies. See the specific enum for more details - // on this field. For example, "clientip:" or - // "project:". - string subject = 2; - - // Free-form text that provides details on the cause of the error. - string description = 3; -} diff --git a/third_party/google/api/servicecontrol/v1/service_controller.proto b/third_party/google/api/servicecontrol/v1/service_controller.proto deleted file mode 100644 index 6e11bcf0f..000000000 --- a/third_party/google/api/servicecontrol/v1/service_controller.proto +++ /dev/null @@ -1,198 +0,0 @@ -// Copyright 2017 Google Inc. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -syntax = "proto3"; - -package google.api.servicecontrol.v1; - -import "google/api/annotations.proto"; -import "google/api/servicecontrol/v1/check_error.proto"; -import "google/api/servicecontrol/v1/operation.proto"; -import "google/rpc/status.proto"; - -option cc_enable_arenas = true; -option go_package = "google.golang.org/genproto/googleapis/api/servicecontrol/v1;servicecontrol"; -option java_multiple_files = true; -option java_outer_classname = "ServiceControllerProto"; -option java_package = "com.google.api.servicecontrol.v1"; -option objc_class_prefix = "GASC"; - -// [Google Service Control API](/service-control/overview) -// -// Lets clients check and report operations against a [managed -// service](https://cloud.google.com/service-management/reference/rpc/google.api/servicemanagement.v1#google.api.servicemanagement.v1.ManagedService). -service ServiceController { - // Checks an operation with Google Service Control to decide whether - // the given operation should proceed. It should be called before the - // operation is executed. - // - // If feasible, the client should cache the check results and reuse them for - // 60 seconds. In case of server errors, the client can rely on the cached - // results for longer time. - // - // NOTE: the [CheckRequest][google.api.servicecontrol.v1.CheckRequest] has the - // size limit of 64KB. - // - // This method requires the `servicemanagement.services.check` permission - // on the specified service. For more information, see - // [Google Cloud IAM](https://cloud.google.com/iam). - rpc Check(CheckRequest) returns (CheckResponse) { - option (google.api.http) = { - post: "/v1/services/{service_name}:check" - body: "*" - }; - } - - // Reports operation results to Google Service Control, such as logs and - // metrics. It should be called after an operation is completed. - // - // If feasible, the client should aggregate reporting data for up to 5 - // seconds to reduce API traffic. Limiting aggregation to 5 seconds is to - // reduce data loss during client crashes. Clients should carefully choose - // the aggregation time window to avoid data loss risk more than 0.01% - // for business and compliance reasons. - // - // NOTE: the [ReportRequest][google.api.servicecontrol.v1.ReportRequest] has - // the size limit of 1MB. - // - // This method requires the `servicemanagement.services.report` permission - // on the specified service. For more information, see - // [Google Cloud IAM](https://cloud.google.com/iam). - rpc Report(ReportRequest) returns (ReportResponse) { - option (google.api.http) = { - post: "/v1/services/{service_name}:report" - body: "*" - }; - } -} - -// Request message for the Check method. -message CheckRequest { - // The service name as specified in its service configuration. For example, - // `"pubsub.googleapis.com"`. - // - // See - // [google.api.Service](https://cloud.google.com/service-management/reference/rpc/google.api#google.api.Service) - // for the definition of a service name. - string service_name = 1; - - // The operation to be checked. - Operation operation = 2; - - // Specifies which version of service configuration should be used to process - // the request. - // - // If unspecified or no matching version can be found, the - // latest one will be used. - string service_config_id = 4; -} - -// Response message for the Check method. -message CheckResponse { - message CheckInfo { - // Consumer info of this check. - ConsumerInfo consumer_info = 2; - } - - // `ConsumerInfo` provides information about the consumer project. - message ConsumerInfo { - // The Google cloud project number, e.g. 1234567890. A value of 0 indicates - // no project number is found. - int64 project_number = 1; - } - - // The same operation_id value used in the - // [CheckRequest][google.api.servicecontrol.v1.CheckRequest]. Used for logging - // and diagnostics purposes. - string operation_id = 1; - - // Indicate the decision of the check. - // - // If no check errors are present, the service should process the operation. - // Otherwise the service should use the list of errors to determine the - // appropriate action. - repeated CheckError check_errors = 2; - - // The actual config id used to process the request. - string service_config_id = 5; - - // Feedback data returned from the server during processing a Check request. - CheckInfo check_info = 6; -} - -// Request message for the Report method. -message ReportRequest { - // The service name as specified in its service configuration. For example, - // `"pubsub.googleapis.com"`. - // - // See - // [google.api.Service](https://cloud.google.com/service-management/reference/rpc/google.api#google.api.Service) - // for the definition of a service name. - string service_name = 1; - - // Operations to be reported. - // - // Typically the service should report one operation per request. - // Putting multiple operations into a single request is allowed, but should - // be used only when multiple operations are natually available at the time - // of the report. - // - // If multiple operations are in a single request, the total request size - // should be no larger than 1MB. See - // [ReportResponse.report_errors][google.api.servicecontrol.v1.ReportResponse.report_errors] - // for partial failure behavior. - repeated Operation operations = 2; - - // Specifies which version of service config should be used to process the - // request. - // - // If unspecified or no matching version can be found, the - // latest one will be used. - string service_config_id = 3; -} - -// Response message for the Report method. -message ReportResponse { - // Represents the processing error of one - // [Operation][google.api.servicecontrol.v1.Operation] in the request. - message ReportError { - // The - // [Operation.operation_id][google.api.servicecontrol.v1.Operation.operation_id] - // value from the request. - string operation_id = 1; - - // Details of the error when processing the - // [Operation][google.api.servicecontrol.v1.Operation]. - google.rpc.Status status = 2; - } - - // Partial failures, one for each `Operation` in the request that failed - // processing. There are three possible combinations of the RPC status: - // - // 1. The combination of a successful RPC status and an empty `report_errors` - // list indicates a complete success where all `Operations` in the - // request are processed successfully. - // 2. The combination of a successful RPC status and a non-empty - // `report_errors` list indicates a partial success where some - // `Operations` in the request succeeded. Each - // `Operation` that failed processing has a corresponding item - // in this list. - // 3. A failed RPC status indicates a general non-deterministic failure. - // When this happens, it's impossible to know which of the - // 'Operations' in the request succeeded or failed. - repeated ReportError report_errors = 1; - - // The actual config id used to process the request. - string service_config_id = 2; -} diff --git a/third_party/google/api/servicemanagement/README.md b/third_party/google/api/servicemanagement/README.md deleted file mode 100644 index e3e36df49..000000000 --- a/third_party/google/api/servicemanagement/README.md +++ /dev/null @@ -1,102 +0,0 @@ -Google Service Management manages a set of *services*. Service -Management allows *service producers* to -publish their services on Google Cloud Platform so that they can be discovered -and used by *service consumers*. It also handles the tasks of tracking -service lifecycle and programming various backend systems -- such as -[Stackdriver Logging](https://cloud.google.com/stackdriver), -[Stackdriver Monitoring](https://cloud.google.com/stackdriver) -- to support -the managed services. - -If you are a service producer, you can use the Google Service Management API -and [Google Cloud SDK (gcloud)](/sdk) to publish and manage your services. -Each managed service has a service configuration which declares various aspects -of the service such as its API surface, along with parameters to configure the -supporting backend -systems, such as logging and monitoring. If you build your service using -[Google Cloud Endpoints](https://cloud.google.com/endpoints/), the service -configuration will be handled automatically. - -If you are a service consumer and want to use a managed service, you can use the -Google Service Management API or [Google Cloud Console](https://console.cloud.google.com) -to activate the -service for your [Google developer project](https://developers.google.com/console/help/new/), -then start using its APIs and functions. - -## Managed services - -REST URL: `https://servicemanagement.googleapis.com/v1/services/{service-name}`
-REST schema is defined [here](/service-management/reference/rest/v1/services). - -A managed service refers to a network service managed by -Service Management. Each managed service has a unique name, such as -`example.googleapis.com`, which must be a valid fully-qualified DNS name, as per -RFC 1035. - -A managed service typically provides some REST APIs and/or other -functions to their service consumers, such as mobile apps or cloud services. - -Service producers can use methods, such as -[services.create](/service-management/reference/rest/v1/services/create), -[services.delete](/service-management/reference/rest/v1/services/delete), -[services.undelete](/service-management/reference/rest/v1/services/undelete), -to manipulate their managed services. - -## Service producers - -A service producer is the Google developer project responsible for publishing -and maintaining a managed service. Each managed service is owned by exactly one -service producer. - -## Service consumers - -A service consumer is a Google developer project that has enabled and can -invoke APIs on a managed service. A managed service can have many service -consumers. - -## Service configuration - -REST URL: `https://servicemanagement.googleapis.com/v1/services/{service-name}/configs/{config_id}`
-REST schema is defined [here](/service-management/reference/rest/v1/services.configs). - -Each managed service is described by a service configuration which covers a wide -range of features, including its name, title, RPC API definitions, -REST API definitions, documentation, authentication, and more. - -To change the configuration of a managed service, the service producer needs to -publish an updated service configuration to Service Management. -Service Management keeps a history of published -service configurations, making it possible to easily retrace how a service's -configuration evolved over time. Service configurations can be published using -the -[services.configs.create](/service-management/reference/rest/v1/services.configs/create) -or [services.configs.submit](/service-management/reference/rest/v1/services.configs/submit) -methods. - -Alternatively, `services.configs.submit` allows publishing an -[OpenAPI](https://github.com/OAI/OpenAPI-Specification) specification, formerly -known as the Swagger Specification, which is automatically converted to a -corresponding service configuration. - -## Service rollout - -REST URL: `https://servicemanagement.googleapis.com/v1/services/{service-name}/rollouts/{rollout-id}`
-REST schema is defined [here](/service-management/reference/rest/v1/services.rollouts). - -A `Rollout` defines how Google Service Management should deploy service -configurations to backend systems and how the configurations take effect at -runtime. It lets service producers specify multiple service configuration -versions to be deployed together, and a strategy that indicates how they -should be used. - -Updating a managed service's configuration can be dangerous, as a configuration -error can lead to a service outage. To mitigate risks, Service Management -supports gradual rollout of service configuration changes. This feature gives -service producers time to identity potential issues and rollback service -configuration changes in case of errors, thus minimizing the customer -impact of bad configurations. For example, you could specify that 5% of traffic -uses configuration 1, while the remaining 95% uses configuration 2. - -Service Management keeps a history of rollouts so that service -producers can undo to previous configuration versions. You can rollback a configuration -by initiating a new `Rollout` that clones a previously submitted -rollout record. \ No newline at end of file diff --git a/third_party/google/api/servicemanagement/artman_servicemanagement_v1.yaml b/third_party/google/api/servicemanagement/artman_servicemanagement_v1.yaml deleted file mode 100644 index cfb5fce21..000000000 --- a/third_party/google/api/servicemanagement/artman_servicemanagement_v1.yaml +++ /dev/null @@ -1,34 +0,0 @@ -common: - api_name: servicemanagement - api_version: v1 - organization_name: google-cloud - proto_deps: - - name: google-common-protos - src_proto_paths: - - v1 - service_yaml: servicemanagement_v1.yaml - gapic_yaml: v1/servicemanagement_gapic.yaml -artifacts: -- name: gapic_config - type: GAPIC_CONFIG -- name: java_gapic - type: GAPIC - language: JAVA -- name: python_gapic - type: GAPIC - language: PYTHON -- name: nodejs_gapic - type: GAPIC - language: NODEJS -- name: php_gapic - type: GAPIC - language: PHP -- name: go_gapic - type: GAPIC - language: GO -- name: ruby_gapic - type: GAPIC - language: RUBY -- name: csharp_gapic - type: GAPIC - language: CSHARP diff --git a/third_party/google/api/servicemanagement/servicemanagement_v1.yaml b/third_party/google/api/servicemanagement/servicemanagement_v1.yaml deleted file mode 100644 index b23a788f8..000000000 --- a/third_party/google/api/servicemanagement/servicemanagement_v1.yaml +++ /dev/null @@ -1,233 +0,0 @@ -type: google.api.Service -config_version: 2 -name: servicemanagement.googleapis.com -title: Google Service Management API - -apis: -- name: google.api.servicemanagement.v1.ServiceManager - -types: -- name: google.api.servicemanagement.v1.ConfigSource -- name: google.api.servicemanagement.v1.ConfigRef -- name: google.api.servicemanagement.v1.OperationMetadata -- name: google.api.servicemanagement.v1.Rollout -- name: google.api.servicemanagement.v1.SubmitConfigSourceResponse -- name: google.api.servicemanagement.v1.UndeleteServiceResponse - -documentation: - summary: |- - Google Service Management allows service producers to publish their services - on Google Cloud Platform so that they can be discovered and used by service - consumers. - overview: |- - Google Service Management manages a set of *services*. Service Management - allows *service producers* to publish their services on Google Cloud - Platform so that they can be discovered and used by *service consumers*. It - also handles the tasks of tracking service lifecycle and programming various - backend systems -- such as [Stackdriver - Logging](https://cloud.google.com/stackdriver), [Stackdriver - Monitoring](https://cloud.google.com/stackdriver) -- to support the managed - services. - - If you are a service producer, you can use the Google Service Management API - and [Google Cloud SDK (gcloud)](/sdk) to publish and manage your services. - Each managed service has a service configuration which declares various - aspects of the service such as its API surface, along with parameters to - configure the supporting backend systems, such as logging and monitoring. If - you build your service using [Google Cloud - Endpoints](https://cloud.google.com/endpoints/), the service configuration - will be handled automatically. - - If you are a service consumer and want to use a managed service, you can use - the Google Service Management API or [Google Cloud - Console](https://console.cloud.google.com) to activate the service for your - [Google developer project](https://developers.google.com/console/help/new/), - then start using its APIs and functions. - - ## Managed services - - REST URL: - `https://servicemanagement.googleapis.com/v1/services/{service-name}`
- REST schema is defined - [here](/service-management/reference/rest/v1/services). - - A managed service refers to a network service managed by Service Management. - Each managed service has a unique name, such as `example.googleapis.com`, - which must be a valid fully-qualified DNS name, as per RFC 1035. - - A managed service typically provides some REST APIs and/or other functions - to their service consumers, such as mobile apps or cloud services. - - Service producers can use methods, such as - [services.create](/service-management/reference/rest/v1/services/create), - [services.delete](/service-management/reference/rest/v1/services/delete), - [services.undelete](/service-management/reference/rest/v1/services/undelete), - to manipulate their managed services. - - ## Service producers - - A service producer is the Google developer project responsible for - publishing and maintaining a managed service. Each managed service is owned - by exactly one service producer. - - ## Service consumers - - A service consumer is a Google developer project that has enabled and can - invoke APIs on a managed service. A managed service can have many service - consumers. - - ## Service configuration - - REST URL: - `https://servicemanagement.googleapis.com/v1/services/{service-name}/configs/{config_id}` -
REST schema is defined - [here](/service-management/reference/rest/v1/services.configs). - - Each managed service is described by a service configuration which covers a - wide range of features, including its name, title, RPC API definitions, REST - API definitions, documentation, authentication, and more. - - To change the configuration of a managed service, the service producer needs - to publish an updated service configuration to Service Management. Service - Management keeps a history of published service configurations, making it - possible to easily retrace how a service's configuration evolved over time. - Service configurations can be published using the - [services.configs.create](/service-management/reference/rest/v1/services.configs/create) - or - [services.configs.submit](/service-management/reference/rest/v1/services.configs/submit) - methods. - - Alternatively, `services.configs.submit` allows publishing an - [OpenAPI](https://github.com/OAI/OpenAPI-Specification) specification, - formerly known as the Swagger Specification, which is automatically - converted to a corresponding service configuration. - - ## Service rollout - - REST URL: - `https://servicemanagement.googleapis.com/v1/services/{service-name}/rollouts/{rollout-id}` -
REST schema is defined - [here](/service-management/reference/rest/v1/services.rollouts). - - A `Rollout` defines how Google Service Management should deploy service - configurations to backend systems and how the configurations take effect at - runtime. It lets service producers specify multiple service configuration - versions to be deployed together, and a strategy that indicates how they - should be used. - - Updating a managed service's configuration can be dangerous, as a - configuration error can lead to a service outage. To mitigate risks, Service - Management supports gradual rollout of service configuration changes. This - feature gives service producers time to identity potential issues and - rollback service configuration changes in case of errors, thus minimizing - the customer impact of bad configurations. For example, you could specify - that 5% of traffic uses configuration 1, while the remaining 95% uses - configuration 2. - - Service Management keeps a history of rollouts so that service producers can - undo to previous configuration versions. You can rollback a configuration by - initiating a new `Rollout` that clones a previously submitted rollout - record. - rules: - - selector: google.longrunning.Operations.ListOperations - description: Lists service operations that match the specified filter in the request. - -backend: - rules: - - selector: google.longrunning.Operations.ListOperations - deadline: 10.0 - - selector: google.longrunning.Operations.GetOperation - deadline: 10.0 - - selector: google.api.servicemanagement.v1.ServiceManager.ListServices - deadline: 10.0 - - selector: google.api.servicemanagement.v1.ServiceManager.GetService - deadline: 10.0 - - selector: google.api.servicemanagement.v1.ServiceManager.CreateService - deadline: 20.0 - - selector: google.api.servicemanagement.v1.ServiceManager.DeleteService - deadline: 10.0 - - selector: google.api.servicemanagement.v1.ServiceManager.UndeleteService - deadline: 10.0 - - selector: google.api.servicemanagement.v1.ServiceManager.ListServiceConfigs - deadline: 10.0 - - selector: google.api.servicemanagement.v1.ServiceManager.GetServiceConfig - deadline: 10.0 - - selector: google.api.servicemanagement.v1.ServiceManager.CreateServiceConfig - deadline: 20.0 - - selector: google.api.servicemanagement.v1.ServiceManager.SubmitConfigSource - deadline: 20.0 - - selector: google.api.servicemanagement.v1.ServiceManager.ListServiceRollouts - deadline: 10.0 - - selector: google.api.servicemanagement.v1.ServiceManager.GetServiceRollout - deadline: 10.0 - - selector: google.api.servicemanagement.v1.ServiceManager.CreateServiceRollout - deadline: 10.0 - - selector: google.api.servicemanagement.v1.ServiceManager.GenerateConfigReport - deadline: 10.0 - - selector: google.api.servicemanagement.v1.ServiceManager.EnableService - deadline: 10.0 - - selector: google.api.servicemanagement.v1.ServiceManager.DisableService - deadline: 10.0 - - selector: google.iam.v1.IAMPolicy.SetIamPolicy - deadline: 10.0 - - selector: google.iam.v1.IAMPolicy.GetIamPolicy - deadline: 10.0 - - selector: google.iam.v1.IAMPolicy.TestIamPermissions - deadline: 10.0 - -http: - rules: - - selector: google.longrunning.Operations.ListOperations - get: /v1/operations - - - selector: google.iam.v1.IAMPolicy.SetIamPolicy - post: '/v1/{resource=services/*}:setIamPolicy' - body: '*' - additional_bindings: - - post: '/v1/{resource=services/*/consumers/*}:setIamPolicy' - body: '*' - - - selector: google.iam.v1.IAMPolicy.GetIamPolicy - post: '/v1/{resource=services/*}:getIamPolicy' - body: '*' - additional_bindings: - - post: '/v1/{resource=services/*/consumers/*}:getIamPolicy' - body: '*' - - - selector: google.iam.v1.IAMPolicy.TestIamPermissions - post: '/v1/{resource=services/*}:testIamPermissions' - body: '*' - additional_bindings: - - post: '/v1/{resource=services/*/consumers/*}:testIamPermissions' - body: '*' - - -authentication: - rules: - - selector: '*' - oauth: - canonical_scopes: |- - https://www.googleapis.com/auth/cloud-platform, - https://www.googleapis.com/auth/service.management - - selector: |- - google.api.servicemanagement.v1.ServiceManager.GetService, - google.api.servicemanagement.v1.ServiceManager.GetServiceConfig, - google.api.servicemanagement.v1.ServiceManager.GetServiceRollout, - google.api.servicemanagement.v1.ServiceManager.ListServiceConfigs, - google.api.servicemanagement.v1.ServiceManager.ListServiceRollouts, - google.api.servicemanagement.v1.ServiceManager.ListServices - oauth: - canonical_scopes: |- - https://www.googleapis.com/auth/cloud-platform, - https://www.googleapis.com/auth/cloud-platform.read-only, - https://www.googleapis.com/auth/service.management, - https://www.googleapis.com/auth/service.management.readonly - - selector: |- - google.iam.v1.IAMPolicy.GetIamPolicy, - google.iam.v1.IAMPolicy.TestIamPermissions - oauth: - canonical_scopes: |- - https://www.googleapis.com/auth/cloud-platform, - https://www.googleapis.com/auth/cloud-platform.read-only, - https://www.googleapis.com/auth/service.management, - https://www.googleapis.com/auth/service.management.readonly diff --git a/third_party/google/api/servicemanagement/v1/resources.proto b/third_party/google/api/servicemanagement/v1/resources.proto deleted file mode 100644 index 1c924849c..000000000 --- a/third_party/google/api/servicemanagement/v1/resources.proto +++ /dev/null @@ -1,299 +0,0 @@ -// Copyright 2018 Google Inc. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -syntax = "proto3"; - -package google.api.servicemanagement.v1; - -import "google/api/annotations.proto"; -import "google/api/config_change.proto"; -import "google/api/metric.proto"; -import "google/api/service.proto"; -import "google/longrunning/operations.proto"; -import "google/protobuf/any.proto"; -import "google/protobuf/field_mask.proto"; -import "google/protobuf/struct.proto"; -import "google/protobuf/timestamp.proto"; -import "google/rpc/status.proto"; - -option csharp_namespace = "Google.Cloud.ServiceManagement.V1"; -option go_package = "google.golang.org/genproto/googleapis/api/servicemanagement/v1;servicemanagement"; -option java_multiple_files = true; -option java_outer_classname = "ResourcesProto"; -option java_package = "com.google.api.servicemanagement.v1"; -option objc_class_prefix = "GASM"; -option php_namespace = "Google\\Cloud\\ServiceManagement\\V1"; - -// The full representation of a Service that is managed by -// Google Service Management. -message ManagedService { - // The name of the service. See the [overview](/service-management/overview) - // for naming requirements. - string service_name = 2; - - // ID of the project that produces and owns this service. - string producer_project_id = 3; -} - -// The metadata associated with a long running operation resource. -message OperationMetadata { - // Represents the status of one operation step. - message Step { - // The short description of the step. - string description = 2; - - // The status code. - Status status = 4; - } - - // Code describes the status of the operation (or one of its steps). - enum Status { - // Unspecifed code. - STATUS_UNSPECIFIED = 0; - - // The operation or step has completed without errors. - DONE = 1; - - // The operation or step has not started yet. - NOT_STARTED = 2; - - // The operation or step is in progress. - IN_PROGRESS = 3; - - // The operation or step has completed with errors. If the operation is - // rollbackable, the rollback completed with errors too. - FAILED = 4; - - // The operation or step has completed with cancellation. - CANCELLED = 5; - } - - // The full name of the resources that this operation is directly - // associated with. - repeated string resource_names = 1; - - // Detailed status information for each step. The order is undetermined. - repeated Step steps = 2; - - // Percentage of completion of this operation, ranging from 0 to 100. - int32 progress_percentage = 3; - - // The start time of the operation. - google.protobuf.Timestamp start_time = 4; -} - -// Represents a diagnostic message (error or warning) -message Diagnostic { - // The kind of diagnostic information possible. - enum Kind { - // Warnings and errors - WARNING = 0; - - // Only errors - ERROR = 1; - } - - // File name and line number of the error or warning. - string location = 1; - - // The kind of diagnostic information provided. - Kind kind = 2; - - // Message describing the error or warning. - string message = 3; -} - -// Represents a source file which is used to generate the service configuration -// defined by `google.api.Service`. -message ConfigSource { - // A unique ID for a specific instance of this message, typically assigned - // by the client for tracking purpose. If empty, the server may choose to - // generate one instead. - string id = 5; - - // Set of source configuration files that are used to generate a service - // configuration (`google.api.Service`). - repeated ConfigFile files = 2; -} - -// Generic specification of a source configuration file -message ConfigFile { - enum FileType { - // Unknown file type. - FILE_TYPE_UNSPECIFIED = 0; - - // YAML-specification of service. - SERVICE_CONFIG_YAML = 1; - - // OpenAPI specification, serialized in JSON. - OPEN_API_JSON = 2; - - // OpenAPI specification, serialized in YAML. - OPEN_API_YAML = 3; - - // FileDescriptorSet, generated by protoc. - // - // To generate, use protoc with imports and source info included. - // For an example test.proto file, the following command would put the value - // in a new file named out.pb. - // - // $protoc --include_imports --include_source_info test.proto -o out.pb - FILE_DESCRIPTOR_SET_PROTO = 4; - - // Uncompiled Proto file. Used for storage and display purposes only, - // currently server-side compilation is not supported. Should match the - // inputs to 'protoc' command used to generated FILE_DESCRIPTOR_SET_PROTO. A - // file of this type can only be included if at least one file of type - // FILE_DESCRIPTOR_SET_PROTO is included. - PROTO_FILE = 6; - } - - // The file name of the configuration file (full or relative path). - string file_path = 1; - - // The bytes that constitute the file. - bytes file_contents = 3; - - // The type of configuration file this represents. - FileType file_type = 4; -} - -// Represents a service configuration with its name and id. -message ConfigRef { - // Resource name of a service config. It must have the following - // format: "services/{service name}/configs/{config id}". - string name = 1; -} - -// Change report associated with a particular service configuration. -// -// It contains a list of ConfigChanges based on the comparison between -// two service configurations. -message ChangeReport { - // List of changes between two service configurations. - // The changes will be alphabetically sorted based on the identifier - // of each change. - // A ConfigChange identifier is a dot separated path to the configuration. - // Example: visibility.rules[selector='LibraryService.CreateBook'].restriction - repeated google.api.ConfigChange config_changes = 1; -} - -// A rollout resource that defines how service configuration versions are pushed -// to control plane systems. Typically, you create a new version of the -// service config, and then create a Rollout to push the service config. -message Rollout { - // Strategy that specifies how clients of Google Service Controller want to - // send traffic to use different config versions. This is generally - // used by API proxy to split traffic based on your configured precentage for - // each config version. - // - // One example of how to gradually rollout a new service configuration using - // this - // strategy: - // Day 1 - // - // Rollout { - // id: "example.googleapis.com/rollout_20160206" - // traffic_percent_strategy { - // percentages: { - // "example.googleapis.com/20160201": 70.00 - // "example.googleapis.com/20160206": 30.00 - // } - // } - // } - // - // Day 2 - // - // Rollout { - // id: "example.googleapis.com/rollout_20160207" - // traffic_percent_strategy: { - // percentages: { - // "example.googleapis.com/20160206": 100.00 - // } - // } - // } - message TrafficPercentStrategy { - // Maps service configuration IDs to their corresponding traffic percentage. - // Key is the service configuration ID, Value is the traffic percentage - // which must be greater than 0.0 and the sum must equal to 100.0. - map percentages = 1; - } - - // Strategy used to delete a service. This strategy is a placeholder only - // used by the system generated rollout to delete a service. - message DeleteServiceStrategy {} - - // Status of a Rollout. - enum RolloutStatus { - // No status specified. - ROLLOUT_STATUS_UNSPECIFIED = 0; - - // The Rollout is in progress. - IN_PROGRESS = 1; - - // The Rollout has completed successfully. - SUCCESS = 2; - - // The Rollout has been cancelled. This can happen if you have overlapping - // Rollout pushes, and the previous ones will be cancelled. - CANCELLED = 3; - - // The Rollout has failed and the rollback attempt has failed too. - FAILED = 4; - - // The Rollout has not started yet and is pending for execution. - PENDING = 5; - - // The Rollout has failed and rolled back to the previous successful - // Rollout. - FAILED_ROLLED_BACK = 6; - } - - // Optional unique identifier of this Rollout. Only lower case letters, digits - // and '-' are allowed. - // - // If not specified by client, the server will generate one. The generated id - // will have the form of , where "date" is the create - // date in ISO 8601 format. "revision number" is a monotonically increasing - // positive number that is reset every day for each service. - // An example of the generated rollout_id is '2016-02-16r1' - string rollout_id = 1; - - // Creation time of the rollout. Readonly. - google.protobuf.Timestamp create_time = 2; - - // The user who created the Rollout. Readonly. - string created_by = 3; - - // The status of this rollout. Readonly. In case of a failed rollout, - // the system will automatically rollback to the current Rollout - // version. Readonly. - RolloutStatus status = 4; - - // Strategy that defines which versions of service configurations should be - // pushed - // and how they should be used at runtime. - oneof strategy { - // Google Service Control selects service configurations based on - // traffic percentage. - TrafficPercentStrategy traffic_percent_strategy = 5; - - // The strategy associated with a rollout to delete a `ManagedService`. - // Readonly. - DeleteServiceStrategy delete_service_strategy = 200; - } - - // The name of the service associated with this Rollout. - string service_name = 8; -} diff --git a/third_party/google/api/servicemanagement/v1/servicemanagement_gapic.yaml b/third_party/google/api/servicemanagement/v1/servicemanagement_gapic.yaml deleted file mode 100644 index ade4187d3..000000000 --- a/third_party/google/api/servicemanagement/v1/servicemanagement_gapic.yaml +++ /dev/null @@ -1,300 +0,0 @@ -type: com.google.api.codegen.ConfigProto -config_schema_version: 1.0.0 -# The settings of generated code in a specific language. -language_settings: - java: - package_name: com.google.cloud.api.servicemanagement.v1 - python: - package_name: google.cloud.api.servicemanagement_v1.gapic - go: - package_name: cloud.google.com/go/api/servicemanagement/apiv1 - csharp: - package_name: Google.Api.Servicemanagement.V1 - ruby: - package_name: Google::Cloud::Api::Servicemanagement::V1 - php: - package_name: Google\Cloud\Api\Servicemanagement\V1 - nodejs: - package_name: servicemanagement.v1 -# A list of API interface configurations. -interfaces: - # The fully qualified name of the API interface. -- name: google.api.servicemanagement.v1.ServiceManager - # A list of resource collection configurations. - # Consists of a name_pattern and an entity_name. - # The name_pattern is a pattern to describe the names of the resources of this - # collection, using the platform's conventions for URI patterns. A generator - # may use this to generate methods to compose and decompose such names. The - # pattern should use named placeholders as in `shelves/{shelf}/books/{book}`; - # those will be taken as hints for the parameter names of the generated - # methods. If empty, no name methods are generated. - # The entity_name is the name to be used as a basis for generated methods and - # classes. - smoke_test: - method: ListServices - init_fields: - - producer_project_id=$PROJECT_ID - collections: [] - # Definition for retryable codes. - retry_codes_def: - - name: idempotent - retry_codes: - - UNAVAILABLE - - name: non_idempotent - retry_codes: [] - # Definition for retry/backoff parameters. - retry_params_def: - - name: default - initial_retry_delay_millis: 100 - retry_delay_multiplier: 1.3 - max_retry_delay_millis: 60000 - initial_rpc_timeout_millis: 20000 - rpc_timeout_multiplier: 1 - max_rpc_timeout_millis: 20000 - total_timeout_millis: 600000 - # A list of method configurations. - # Common properties: - # - # name - The simple name of the method. - # - # flattening - Specifies the configuration for parameter flattening. - # Describes the parameter groups for which a generator should produce method - # overloads which allow a client to directly pass request message fields as - # method parameters. This information may or may not be used, depending on - # the target language. - # Consists of groups, which each represent a list of parameters to be - # flattened. Each parameter listed must be a field of the request message. - # - # required_fields - Fields that are always required for a request to be - # valid. - # - # resource_name_treatment - An enum that specifies how to treat the resource - # name formats defined in the field_name_patterns and - # response_field_name_patterns fields. - # UNSET: default value - # NONE: the collection configs will not be used by the generated code. - # VALIDATE: string fields will be validated by the client against the - # specified resource name formats. - # STATIC_TYPES: the client will use generated types for resource names. - # - # page_streaming - Specifies the configuration for paging. - # Describes information for generating a method which transforms a paging - # list RPC into a stream of resources. - # Consists of a request and a response. - # The request specifies request information of the list method. It defines - # which fields match the paging pattern in the request. The request consists - # of a page_size_field and a token_field. The page_size_field is the name of - # the optional field specifying the maximum number of elements to be - # returned in the response. The token_field is the name of the field in the - # request containing the page token. - # The response specifies response information of the list method. It defines - # which fields match the paging pattern in the response. The response - # consists of a token_field and a resources_field. The token_field is the - # name of the field in the response containing the next page token. The - # resources_field is the name of the field in the response containing the - # list of resources belonging to the page. - # - # retry_codes_name - Specifies the configuration for retryable codes. The - # name must be defined in interfaces.retry_codes_def. - # - # retry_params_name - Specifies the configuration for retry/backoff - # parameters. The name must be defined in interfaces.retry_params_def. - # - # field_name_patterns - Maps the field name of the request type to - # entity_name of interfaces.collections. - # Specifies the string pattern that the field must follow. - # - # timeout_millis - Specifies the default timeout for a non-retrying call. If - # the call is retrying, refer to retry_params_name instead. - methods: - - name: ListServices - flattening: - groups: - - parameters: - - producer_project_id - - consumer_id - required_fields: - page_streaming: - request: - page_size_field: page_size - token_field: page_token - response: - token_field: next_page_token - resources_field: services - retry_codes_name: idempotent - retry_params_name: default - timeout_millis: 10000 - - name: GetService - flattening: - groups: - - parameters: - - service_name - required_fields: - - service_name - retry_codes_name: idempotent - retry_params_name: default - timeout_millis: 10000 - - name: CreateService - flattening: - groups: - - parameters: - - service - required_fields: - - service - retry_codes_name: non_idempotent - retry_params_name: default - timeout_millis: 20000 - - name: DeleteService - flattening: - groups: - - parameters: - - service_name - required_fields: - - service_name - retry_codes_name: idempotent - retry_params_name: default - timeout_millis: 60000 - - name: UndeleteService - flattening: - groups: - - parameters: - - service_name - required_fields: - - service_name - retry_codes_name: non_idempotent - retry_params_name: default - # REVIEW: Could this operation take a long time? - timeout_millis: 60000 - - name: ListServiceConfigs - flattening: - groups: - - parameters: - - service_name - required_fields: - - service_name - page_streaming: - request: - page_size_field: page_size - token_field: page_token - response: - token_field: next_page_token - resources_field: service_configs - retry_codes_name: idempotent - retry_params_name: default - timeout_millis: 10000 - - name: GetServiceConfig - flattening: - groups: - - parameters: - - service_name - - config_id - - view - required_fields: - - service_name - - config_id - retry_codes_name: idempotent - retry_params_name: default - timeout_millis: 10000 - - name: CreateServiceConfig - flattening: - groups: - - parameters: - - service_name - - service_config - required_fields: - - service_name - - service_config - retry_codes_name: non_idempotent - retry_params_name: default - timeout_millis: 20000 - - name: SubmitConfigSource - flattening: - groups: - - parameters: - - service_name - - config_source - - validate_only - required_fields: - - service_name - - config_source - retry_codes_name: non_idempotent - retry_params_name: default - timeout_millis: 10000 - - name: ListServiceRollouts - flattening: - groups: - - parameters: - - service_name - - filter - required_fields: - - service_name - page_streaming: - request: - page_size_field: page_size - token_field: page_token - response: - token_field: next_page_token - resources_field: rollouts - retry_codes_name: idempotent - retry_params_name: default - timeout_millis: 10000 - - name: GetServiceRollout - flattening: - groups: - - parameters: - - service_name - - rollout_id - required_fields: - - service_name - - rollout_id - retry_codes_name: idempotent - retry_params_name: default - timeout_millis: 10000 - - name: CreateServiceRollout - flattening: - groups: - - parameters: - - service_name - - rollout - required_fields: - - service_name - - rollout - retry_codes_name: non_idempotent - retry_params_name: default - timeout_millis: 10000 - - name: GenerateConfigReport - flattening: - groups: - - parameters: - - new_config - - old_config - required_fields: - - new_config - - old_config - retry_codes_name: non_idempotent - retry_params_name: default - timeout_millis: 10000 - - name: EnableService - flattening: - groups: - - parameters: - - service_name - - consumer_id - required_fields: - - service_name - - consumer_id - retry_codes_name: idempotent - retry_params_name: default - timeout_millis: 10000 - - name: DisableService - flattening: - groups: - - parameters: - - service_name - - consumer_id - required_fields: - - service_name - - consumer_id - retry_codes_name: idempotent - retry_params_name: default - timeout_millis: 10000 diff --git a/third_party/google/api/servicemanagement/v1/servicemanager.proto b/third_party/google/api/servicemanagement/v1/servicemanager.proto deleted file mode 100644 index 02d506665..000000000 --- a/third_party/google/api/servicemanagement/v1/servicemanager.proto +++ /dev/null @@ -1,503 +0,0 @@ -// Copyright 2018 Google Inc. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -syntax = "proto3"; - -package google.api.servicemanagement.v1; - -import "google/api/annotations.proto"; -import "google/api/service.proto"; -import "google/api/servicemanagement/v1/resources.proto"; -import "google/longrunning/operations.proto"; -import "google/protobuf/any.proto"; -import "google/protobuf/field_mask.proto"; -import "google/protobuf/struct.proto"; -import "google/rpc/status.proto"; - -option csharp_namespace = "Google.Cloud.ServiceManagement.V1"; -option go_package = "google.golang.org/genproto/googleapis/api/servicemanagement/v1;servicemanagement"; -option java_multiple_files = true; -option java_outer_classname = "ServiceManagerProto"; -option java_package = "com.google.api.servicemanagement.v1"; -option objc_class_prefix = "GASM"; -option php_namespace = "Google\\Cloud\\ServiceManagement\\V1"; - -// [Google Service Management API](/service-management/overview) -service ServiceManager { - // Lists managed services. - // - // Returns all public services. For authenticated users, also returns all - // services the calling user has "servicemanagement.services.get" permission - // for. - // - // **BETA:** If the caller specifies the `consumer_id`, it returns only the - // services enabled on the consumer. The `consumer_id` must have the format - // of "project:{PROJECT-ID}". - rpc ListServices(ListServicesRequest) returns (ListServicesResponse) { - option (google.api.http) = { - get: "/v1/services" - }; - } - - // Gets a managed service. Authentication is required unless the service is - // public. - rpc GetService(GetServiceRequest) returns (ManagedService) { - option (google.api.http) = { - get: "/v1/services/{service_name}" - }; - } - - // Creates a new managed service. - // Please note one producer project can own no more than 20 services. - // - // Operation - rpc CreateService(CreateServiceRequest) - returns (google.longrunning.Operation) { - option (google.api.http) = { - post: "/v1/services" - body: "service" - }; - } - - // Deletes a managed service. This method will change the service to the - // `Soft-Delete` state for 30 days. Within this period, service producers may - // call - // [UndeleteService][google.api.servicemanagement.v1.ServiceManager.UndeleteService] - // to restore the service. After 30 days, the service will be permanently - // deleted. - // - // Operation - rpc DeleteService(DeleteServiceRequest) - returns (google.longrunning.Operation) { - option (google.api.http) = { - delete: "/v1/services/{service_name}" - }; - } - - // Revives a previously deleted managed service. The method restores the - // service using the configuration at the time the service was deleted. - // The target service must exist and must have been deleted within the - // last 30 days. - // - // Operation - rpc UndeleteService(UndeleteServiceRequest) - returns (google.longrunning.Operation) { - option (google.api.http) = { - post: "/v1/services/{service_name}:undelete" - }; - } - - // Lists the history of the service configuration for a managed service, - // from the newest to the oldest. - rpc ListServiceConfigs(ListServiceConfigsRequest) - returns (ListServiceConfigsResponse) { - option (google.api.http) = { - get: "/v1/services/{service_name}/configs" - }; - } - - // Gets a service configuration (version) for a managed service. - rpc GetServiceConfig(GetServiceConfigRequest) returns (google.api.Service) { - option (google.api.http) = { - get: "/v1/services/{service_name}/configs/{config_id}" - additional_bindings { get: "/v1/services/{service_name}/config" } - }; - } - - // Creates a new service configuration (version) for a managed service. - // This method only stores the service configuration. To roll out the service - // configuration to backend systems please call - // [CreateServiceRollout][google.api.servicemanagement.v1.ServiceManager.CreateServiceRollout]. - // - // Only the 100 most recent service configurations and ones referenced by - // existing rollouts are kept for each service. The rest will be deleted - // eventually. - rpc CreateServiceConfig(CreateServiceConfigRequest) - returns (google.api.Service) { - option (google.api.http) = { - post: "/v1/services/{service_name}/configs" - body: "service_config" - }; - } - - // Creates a new service configuration (version) for a managed service based - // on - // user-supplied configuration source files (for example: OpenAPI - // Specification). This method stores the source configurations as well as the - // generated service configuration. To rollout the service configuration to - // other services, - // please call - // [CreateServiceRollout][google.api.servicemanagement.v1.ServiceManager.CreateServiceRollout]. - // - // Only the 100 most recent configuration sources and ones referenced by - // existing service configurtions are kept for each service. The rest will be - // deleted eventually. - // - // Operation - rpc SubmitConfigSource(SubmitConfigSourceRequest) - returns (google.longrunning.Operation) { - option (google.api.http) = { - post: "/v1/services/{service_name}/configs:submit" - body: "*" - }; - } - - // Lists the history of the service configuration rollouts for a managed - // service, from the newest to the oldest. - rpc ListServiceRollouts(ListServiceRolloutsRequest) - returns (ListServiceRolloutsResponse) { - option (google.api.http) = { - get: "/v1/services/{service_name}/rollouts" - }; - } - - // Gets a service configuration - // [rollout][google.api.servicemanagement.v1.Rollout]. - rpc GetServiceRollout(GetServiceRolloutRequest) returns (Rollout) { - option (google.api.http) = { - get: "/v1/services/{service_name}/rollouts/{rollout_id}" - }; - } - - // Creates a new service configuration rollout. Based on rollout, the - // Google Service Management will roll out the service configurations to - // different backend services. For example, the logging configuration will be - // pushed to Google Cloud Logging. - // - // Please note that any previous pending and running Rollouts and associated - // Operations will be automatically cancelled so that the latest Rollout will - // not be blocked by previous Rollouts. - // - // Only the 100 most recent (in any state) and the last 10 successful (if not - // already part of the set of 100 most recent) rollouts are kept for each - // service. The rest will be deleted eventually. - // - // Operation - rpc CreateServiceRollout(CreateServiceRolloutRequest) - returns (google.longrunning.Operation) { - option (google.api.http) = { - post: "/v1/services/{service_name}/rollouts" - body: "rollout" - }; - } - - // Generates and returns a report (errors, warnings and changes from - // existing configurations) associated with - // GenerateConfigReportRequest.new_value - // - // If GenerateConfigReportRequest.old_value is specified, - // GenerateConfigReportRequest will contain a single ChangeReport based on the - // comparison between GenerateConfigReportRequest.new_value and - // GenerateConfigReportRequest.old_value. - // If GenerateConfigReportRequest.old_value is not specified, this method - // will compare GenerateConfigReportRequest.new_value with the last pushed - // service configuration. - rpc GenerateConfigReport(GenerateConfigReportRequest) - returns (GenerateConfigReportResponse) { - option (google.api.http) = { - post: "/v1/services:generateConfigReport" - body: "*" - }; - } - - // Enables a [service][google.api.servicemanagement.v1.ManagedService] for a - // project, so it can be used for the project. See [Cloud Auth - // Guide](https://cloud.google.com/docs/authentication) for more information. - // - // Operation - rpc EnableService(EnableServiceRequest) - returns (google.longrunning.Operation) { - option (google.api.http) = { - post: "/v1/services/{service_name}:enable" - body: "*" - }; - } - - // Disables a [service][google.api.servicemanagement.v1.ManagedService] for a - // project, so it can no longer be be used for the project. It prevents - // accidental usage that may cause unexpected billing charges or security - // leaks. - // - // Operation - rpc DisableService(DisableServiceRequest) - returns (google.longrunning.Operation) { - option (google.api.http) = { - post: "/v1/services/{service_name}:disable" - body: "*" - }; - } -} - -// Request message for `ListServices` method. -message ListServicesRequest { - // Include services produced by the specified project. - string producer_project_id = 1; - - // Requested size of the next page of data. - int32 page_size = 5; - - // Token identifying which result to start with; returned by a previous list - // call. - string page_token = 6; - - // Include services consumed by the specified consumer. - // - // The Google Service Management implementation accepts the following - // forms: - // - project: - string consumer_id = 7; -} - -// Response message for `ListServices` method. -message ListServicesResponse { - // The returned services will only have the name field set. - repeated ManagedService services = 1; - - // Token that can be passed to `ListServices` to resume a paginated query. - string next_page_token = 2; -} - -// Request message for `GetService` method. -message GetServiceRequest { - // The name of the service. See the `ServiceManager` overview for naming - // requirements. For example: `example.googleapis.com`. - string service_name = 1; -} - -// Request message for CreateService method. -message CreateServiceRequest { - // Initial values for the service resource. - ManagedService service = 1; -} - -// Request message for DeleteService method. -message DeleteServiceRequest { - // The name of the service. See the [overview](/service-management/overview) - // for naming requirements. For example: `example.googleapis.com`. - string service_name = 1; -} - -// Request message for UndeleteService method. -message UndeleteServiceRequest { - // The name of the service. See the [overview](/service-management/overview) - // for naming requirements. For example: `example.googleapis.com`. - string service_name = 1; -} - -// Response message for UndeleteService method. -message UndeleteServiceResponse { - // Revived service resource. - ManagedService service = 1; -} - -// Request message for GetServiceConfig method. -message GetServiceConfigRequest { - enum ConfigView { - // Server response includes all fields except SourceInfo. - BASIC = 0; - - // Server response includes all fields including SourceInfo. - // SourceFiles are of type 'google.api.servicemanagement.v1.ConfigFile' - // and are only available for configs created using the - // SubmitConfigSource method. - FULL = 1; - } - - // The name of the service. See the [overview](/service-management/overview) - // for naming requirements. For example: `example.googleapis.com`. - string service_name = 1; - - // The id of the service configuration resource. - string config_id = 2; - - // Specifies which parts of the Service Config should be returned in the - // response. - ConfigView view = 3; -} - -// Request message for ListServiceConfigs method. -message ListServiceConfigsRequest { - // The name of the service. See the [overview](/service-management/overview) - // for naming requirements. For example: `example.googleapis.com`. - string service_name = 1; - - // The token of the page to retrieve. - string page_token = 2; - - // The max number of items to include in the response list. - int32 page_size = 3; -} - -// Response message for ListServiceConfigs method. -message ListServiceConfigsResponse { - // The list of service configuration resources. - repeated google.api.Service service_configs = 1; - - // The token of the next page of results. - string next_page_token = 2; -} - -// Request message for CreateServiceConfig method. -message CreateServiceConfigRequest { - // The name of the service. See the [overview](/service-management/overview) - // for naming requirements. For example: `example.googleapis.com`. - string service_name = 1; - - // The service configuration resource. - google.api.Service service_config = 2; -} - -// Request message for SubmitConfigSource method. -message SubmitConfigSourceRequest { - // The name of the service. See the [overview](/service-management/overview) - // for naming requirements. For example: `example.googleapis.com`. - string service_name = 1; - - // The source configuration for the service. - ConfigSource config_source = 2; - - // Optional. If set, this will result in the generation of a - // `google.api.Service` configuration based on the `ConfigSource` provided, - // but the generated config and the sources will NOT be persisted. - bool validate_only = 3; -} - -// Response message for SubmitConfigSource method. -message SubmitConfigSourceResponse { - // The generated service configuration. - google.api.Service service_config = 1; -} - -// Request message for 'CreateServiceRollout' -message CreateServiceRolloutRequest { - // The name of the service. See the [overview](/service-management/overview) - // for naming requirements. For example: `example.googleapis.com`. - string service_name = 1; - - // The rollout resource. The `service_name` field is output only. - Rollout rollout = 2; -} - -// Request message for 'ListServiceRollouts' -message ListServiceRolloutsRequest { - // The name of the service. See the [overview](/service-management/overview) - // for naming requirements. For example: `example.googleapis.com`. - string service_name = 1; - - // The token of the page to retrieve. - string page_token = 2; - - // The max number of items to include in the response list. - int32 page_size = 3; - - // Use `filter` to return subset of rollouts. - // The following filters are supported: - // -- To limit the results to only those in - // [status](google.api.servicemanagement.v1.RolloutStatus) 'SUCCESS', - // use filter='status=SUCCESS' - // -- To limit the results to those in - // [status](google.api.servicemanagement.v1.RolloutStatus) 'CANCELLED' - // or 'FAILED', use filter='status=CANCELLED OR status=FAILED' - string filter = 4; -} - -// Response message for ListServiceRollouts method. -message ListServiceRolloutsResponse { - // The list of rollout resources. - repeated Rollout rollouts = 1; - - // The token of the next page of results. - string next_page_token = 2; -} - -// Request message for GetServiceRollout method. -message GetServiceRolloutRequest { - // The name of the service. See the [overview](/service-management/overview) - // for naming requirements. For example: `example.googleapis.com`. - string service_name = 1; - - // The id of the rollout resource. - string rollout_id = 2; -} - -// Request message for EnableService method. -message EnableServiceRequest { - // Name of the service to enable. Specifying an unknown service name will - // cause the request to fail. - string service_name = 1; - - // The identity of consumer resource which service enablement will be - // applied to. - // - // The Google Service Management implementation accepts the following - // forms: - // - "project:" - // - // Note: this is made compatible with - // google.api.servicecontrol.v1.Operation.consumer_id. - string consumer_id = 2; -} - -// Request message for DisableService method. -message DisableServiceRequest { - // Name of the service to disable. Specifying an unknown service name - // will cause the request to fail. - string service_name = 1; - - // The identity of consumer resource which service disablement will be - // applied to. - // - // The Google Service Management implementation accepts the following - // forms: - // - "project:" - // - // Note: this is made compatible with - // google.api.servicecontrol.v1.Operation.consumer_id. - string consumer_id = 2; -} - -// Request message for GenerateConfigReport method. -message GenerateConfigReportRequest { - // Service configuration for which we want to generate the report. - // For this version of API, the supported types are - // [google.api.servicemanagement.v1.ConfigRef][google.api.servicemanagement.v1.ConfigRef], - // [google.api.servicemanagement.v1.ConfigSource][google.api.servicemanagement.v1.ConfigSource], - // and [google.api.Service][google.api.Service] - google.protobuf.Any new_config = 1; - - // Service configuration against which the comparison will be done. - // For this version of API, the supported types are - // [google.api.servicemanagement.v1.ConfigRef][google.api.servicemanagement.v1.ConfigRef], - // [google.api.servicemanagement.v1.ConfigSource][google.api.servicemanagement.v1.ConfigSource], - // and [google.api.Service][google.api.Service] - google.protobuf.Any old_config = 2; -} - -// Response message for GenerateConfigReport method. -message GenerateConfigReportResponse { - // Name of the service this report belongs to. - string service_name = 1; - - // ID of the service configuration this report belongs to. - string id = 2; - - // list of ChangeReport, each corresponding to comparison between two - // service configurations. - repeated ChangeReport change_reports = 3; - - // Errors / Linter warnings associated with the service definition this - // report - // belongs to. - repeated Diagnostic diagnostics = 4; -} diff --git a/third_party/google/api/source_info.proto b/third_party/google/api/source_info.proto deleted file mode 100644 index 5954143de..000000000 --- a/third_party/google/api/source_info.proto +++ /dev/null @@ -1,32 +0,0 @@ -// Copyright 2019 Google LLC. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -// - -syntax = "proto3"; - -package google.api; - -import "google/protobuf/any.proto"; - -option go_package = "google.golang.org/genproto/googleapis/api/serviceconfig;serviceconfig"; -option java_multiple_files = true; -option java_outer_classname = "SourceInfoProto"; -option java_package = "com.google.api"; -option objc_class_prefix = "GAPI"; - -// Source information used to create a Service Config -message SourceInfo { - // All files used during config generation. - repeated google.protobuf.Any source_files = 1; -} diff --git a/third_party/google/api/system_parameter.proto b/third_party/google/api/system_parameter.proto deleted file mode 100644 index 740a5538b..000000000 --- a/third_party/google/api/system_parameter.proto +++ /dev/null @@ -1,96 +0,0 @@ -// Copyright 2019 Google LLC. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -// - -syntax = "proto3"; - -package google.api; - -option go_package = "google.golang.org/genproto/googleapis/api/serviceconfig;serviceconfig"; -option java_multiple_files = true; -option java_outer_classname = "SystemParameterProto"; -option java_package = "com.google.api"; -option objc_class_prefix = "GAPI"; - -// ### System parameter configuration -// -// A system parameter is a special kind of parameter defined by the API -// system, not by an individual API. It is typically mapped to an HTTP header -// and/or a URL query parameter. This configuration specifies which methods -// change the names of the system parameters. -message SystemParameters { - // Define system parameters. - // - // The parameters defined here will override the default parameters - // implemented by the system. If this field is missing from the service - // config, default system parameters will be used. Default system parameters - // and names is implementation-dependent. - // - // Example: define api key for all methods - // - // system_parameters - // rules: - // - selector: "*" - // parameters: - // - name: api_key - // url_query_parameter: api_key - // - // - // Example: define 2 api key names for a specific method. - // - // system_parameters - // rules: - // - selector: "/ListShelves" - // parameters: - // - name: api_key - // http_header: Api-Key1 - // - name: api_key - // http_header: Api-Key2 - // - // **NOTE:** All service configuration rules follow "last one wins" order. - repeated SystemParameterRule rules = 1; -} - -// Define a system parameter rule mapping system parameter definitions to -// methods. -message SystemParameterRule { - // Selects the methods to which this rule applies. Use '*' to indicate all - // methods in all APIs. - // - // Refer to [selector][google.api.DocumentationRule.selector] for syntax details. - string selector = 1; - - // Define parameters. Multiple names may be defined for a parameter. - // For a given method call, only one of them should be used. If multiple - // names are used the behavior is implementation-dependent. - // If none of the specified names are present the behavior is - // parameter-dependent. - repeated SystemParameter parameters = 2; -} - -// Define a parameter's name and location. The parameter may be passed as either -// an HTTP header or a URL query parameter, and if both are passed the behavior -// is implementation-dependent. -message SystemParameter { - // Define the name of the parameter, such as "api_key" . It is case sensitive. - string name = 1; - - // Define the HTTP header name to use for the parameter. It is case - // insensitive. - string http_header = 2; - - // Define the URL query parameter name to use for the parameter. It is case - // sensitive. - string url_query_parameter = 3; -} diff --git a/third_party/google/api/usage.proto b/third_party/google/api/usage.proto deleted file mode 100644 index 6ab4e408c..000000000 --- a/third_party/google/api/usage.proto +++ /dev/null @@ -1,90 +0,0 @@ -// Copyright 2019 Google LLC. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -// - -syntax = "proto3"; - -package google.api; - -option go_package = "google.golang.org/genproto/googleapis/api/serviceconfig;serviceconfig"; -option java_multiple_files = true; -option java_outer_classname = "UsageProto"; -option java_package = "com.google.api"; -option objc_class_prefix = "GAPI"; - -// Configuration controlling usage of a service. -message Usage { - // Requirements that must be satisfied before a consumer project can use the - // service. Each requirement is of the form /; - // for example 'serviceusage.googleapis.com/billing-enabled'. - repeated string requirements = 1; - - // A list of usage rules that apply to individual API methods. - // - // **NOTE:** All service configuration rules follow "last one wins" order. - repeated UsageRule rules = 6; - - // The full resource name of a channel used for sending notifications to the - // service producer. - // - // Google Service Management currently only supports - // [Google Cloud Pub/Sub](https://cloud.google.com/pubsub) as a notification - // channel. To use Google Cloud Pub/Sub as the channel, this must be the name - // of a Cloud Pub/Sub topic that uses the Cloud Pub/Sub topic name format - // documented in https://cloud.google.com/pubsub/docs/overview. - string producer_notification_channel = 7; -} - -// Usage configuration rules for the service. -// -// NOTE: Under development. -// -// -// Use this rule to configure unregistered calls for the service. Unregistered -// calls are calls that do not contain consumer project identity. -// (Example: calls that do not contain an API key). -// By default, API methods do not allow unregistered calls, and each method call -// must be identified by a consumer project identity. Use this rule to -// allow/disallow unregistered calls. -// -// Example of an API that wants to allow unregistered calls for entire service. -// -// usage: -// rules: -// - selector: "*" -// allow_unregistered_calls: true -// -// Example of a method that wants to allow unregistered calls. -// -// usage: -// rules: -// - selector: "google.example.library.v1.LibraryService.CreateBook" -// allow_unregistered_calls: true -message UsageRule { - // Selects the methods to which this rule applies. Use '*' to indicate all - // methods in all APIs. - // - // Refer to [selector][google.api.DocumentationRule.selector] for syntax details. - string selector = 1; - - // If true, the selected method allows unregistered calls, e.g. calls - // that don't identify any user or application. - bool allow_unregistered_calls = 2; - - // If true, the selected method should skip service control and the control - // plane features, such as quota and billing, will not be available. - // This flag is used by Google Cloud Endpoints to bypass checks for internal - // methods, such as service health check methods. - bool skip_service_control = 3; -} diff --git a/tool/kratos-gen-bts/README.md b/tool/kratos-gen-bts/README.md deleted file mode 100644 index 82f6b0af2..000000000 --- a/tool/kratos-gen-bts/README.md +++ /dev/null @@ -1,48 +0,0 @@ -#### genbts - -> 缓存代码生成 - -##### 项目简介 - -从缓存中获取数据 如果miss则调用回源函数从数据源获取 然后塞入缓存 - -支持以下功能: - -- 单飞限制回源并发 防止打爆数据源 -- 空缓存 防止缓存穿透 -- 分批获取数据 降低延时 -- 默认异步加缓存 可选同步加缓存 -- prometheus回源比监控 -- 多行注释生成代码 -- 支持分页(限单key模板) -- 自定义注释 -- 支持忽略参数 - -##### 使用方式: -1. 在dao package中 增加注解 //go:generate kratos tool genbts 定义bts接口 声明需要的方法 -2. 在dao 文件夹中执行 go generate命令 将会生成相应的缓存代码 -3. 调用生成的XXX方法 -4. 示例见testdata/dao.go - -要求: -dao里面需要有cache对象 代码会调用d.cache来新增缓存 -需要实现代码中所需的方法 每一个缓存方法都需要实现以下方法: -从缓存中获取数据 名称为Cache+方法名 函数定义和声明一致 -从数据源(db/api/...)获取数据 名称为Raw+方法 函数定义和声明一致 -存入缓存方法 名称为AddCache+方法名 函数定义为 func AddCache方法名(c context.Context, ...) (error) - -##### 注解参数: -| 参数名称 | 默认值 | 说明 | 示例 | -| ---------------- | ------ | ------------------------------------------------------------ | ------------------------------------------------------------ | -| -nullcache | | 空指针对象(存正常业务不会出现的内容 id的话像是-1这样的) | &Demo{ID:-1} 或-1 或"null" | -| -check_null_code | | 开启空缓存并且value为指针对象时必填 用于判断是否是空缓存 $来指代对象名 | `-check_null_code=$!=nil&&$.ID==-1 或 $ == -1` | -| -cache_err |continue| 缓存出错的时候的行为 continue: 继续执行 break: 抛出错误 方法返回|break| -| -batch | | (限多key模板) 批量获取数据 每组大小 | 100 | -| -max_group | | (限多key模板)批量获取数据 最大组数量 | 10 | -| -batch_err | break | (限多key模板)批量获取数据回源错误的时候 降级继续请求(continue)还是直接返回(break) | break 或 continue | -| -singleflight | false | 是否开启单飞(开启后生成函数会多一个单飞名称参数 生成的代码会调用d.cacheSFNAME方法获取单飞的key) | true | -| -sync | false | 是否同步增加缓存 | false | -| -paging | false | (限单key模板)分页 数据源应返回2个值 第一个为对外数据 第二个为全量数据 用于新增缓存 | false | -| -ignores | | 用于依赖的三个方法参数和主方法参数不一致的情况. 忽略方法的某些参数 用\|分隔方法逗号分隔参数 | pn,ps\|pn\|origin 表示"缓存获取"方法忽略pn,ps两个参数 回源方法忽略pn参数 加缓存方法忽略origin参数 | -| -custom_method | false | 自定义方法名 \|分隔 缓存获取方法名\|回源方法名\|增加缓存方法名 | d.mc.AddDemo\|d.mysql.Demo\|d.mc.AddDemo | -| -struct_name | dao | 所属结构体名称 | Dao| \ No newline at end of file diff --git a/tool/kratos-gen-bts/header_template.go b/tool/kratos-gen-bts/header_template.go deleted file mode 100644 index 613e886b7..000000000 --- a/tool/kratos-gen-bts/header_template.go +++ /dev/null @@ -1,33 +0,0 @@ -package main - -var _headerTemplate = ` -// Code generated by kratos tool genbts. DO NOT EDIT. - -NEWLINE -/* - Package {{.PkgName}} is a generated cache proxy package. - It is generated from: - ARGS -*/ -NEWLINE - -package {{.PkgName}} - -import ( - "context" - {{if .EnableBatch }}"sync"{{end}} -NEWLINE - "github.com/go-kratos/kratos/pkg/cache" - {{if .EnableBatch }}"github.com/go-kratos/kratos/pkg/sync/errgroup"{{end}} - {{.ImportPackage}} -NEWLINE - {{if .EnableSingleFlight}} "golang.org/x/sync/singleflight" {{end}} -) - -{{if .UseBTS}} -var _ _bts -{{end }} -{{if .EnableSingleFlight}} -var cacheSingleFlights = [SFCOUNT]*singleflight.Group{SFINIT} -{{end }} -` diff --git a/tool/kratos-gen-bts/main.go b/tool/kratos-gen-bts/main.go deleted file mode 100644 index f1bd7f624..000000000 --- a/tool/kratos-gen-bts/main.go +++ /dev/null @@ -1,507 +0,0 @@ -package main - -import ( - "bytes" - "flag" - "fmt" - "go/ast" - "io/ioutil" - "log" - "os" - "path/filepath" - "regexp" - "runtime" - "strconv" - "strings" - "text/template" - - "github.com/go-kratos/kratos/tool/pkg" -) - -var ( - // arguments - singleFlight = flag.Bool("singleflight", false, "enable singleflight") - nullCache = flag.String("nullcache", "", "null cache") - checkNullCode = flag.String("check_null_code", "", "check null code") - cacheErr = flag.String("cache_err", "continue", "cache err to continue or break") - batchSize = flag.Int("batch", 0, "batch size") - batchErr = flag.String("batch_err", "break", "batch err to continue or break") - maxGroup = flag.Int("max_group", 0, "max group size") - sync = flag.Bool("sync", false, "add cache in sync way.") - paging = flag.Bool("paging", false, "use paging in single template") - ignores = flag.String("ignores", "", "ignore params") - customMethod = flag.String("custom_method", "", "自定义方法名 |分隔: 缓存|回源|增加缓存") - structName = flag.String("struct_name", "dao", "struct name") - - numberTypes = []string{"int", "int8", "int16", "int32", "int64", "float32", "float64", "uint", "uint8", "uint16", "uint32", "uint64"} - simpleTypes = []string{"int", "int8", "int16", "int32", "int64", "float32", "float64", "uint", "uint8", "uint16", "uint32", "uint64", "bool", "string", "[]byte"} - optionNames = []string{"singleflight", "nullcache", "check_null_code", "batch", "max_group", "sync", "paging", "ignores", "batch_err", "custom_method", "cache_err", "struct_name"} - optionNamesMap = map[string]bool{} - interfaceName string -) - -const ( - _multiTpl = 1 - _singleTpl = 2 - _noneTpl = 3 -) - -func resetFlag() { - *singleFlight = false - *nullCache = "" - *checkNullCode = "" - *batchSize = 0 - *maxGroup = 0 - *sync = false - *paging = false - *batchErr = "break" - *cacheErr = "continue" - *ignores = "" - *customMethod = "" - *structName = "dao" -} - -// options options -type options struct { - name string - keyType string - valueType string - cacheFunc string - rawFunc string - addCacheFunc string - template int - SimpleValue bool - NumberValue bool - GoValue bool - ZeroValue string - ImportPackage string - importPackages []string - Args string - PkgName string - EnableSingleFlight bool - NullCache string - EnableNullCache bool - GroupSize int - MaxGroup int - EnableBatch bool - BatchErrBreak bool - Sync bool - CheckNullCode string - ExtraArgsType string - ExtraArgs string - ExtraCacheArgs string - ExtraRawArgs string - ExtraAddCacheArgs string - EnablePaging bool - Comment string - CustomMethod string - IDName string - CacheErrContinue bool - StructName string - hasDec bool - UseBTS bool -} - -func getOptions(opt *options, comment string) { - os.Args = []string{os.Args[0]} - if regexp.MustCompile(`\s+//\s*bts:.+`).Match([]byte(comment)) { - args := strings.Split(pkg.RegexpReplace(`//\s*bts:(?P.+)`, comment, "$arg"), " ") - for _, arg := range args { - arg = strings.TrimSpace(arg) - if arg != "" { - // validate option name - argName := pkg.RegexpReplace(`-(?P[\w_-]+)=.+`, arg, "$name") - if !optionNamesMap[argName] { - log.Fatalf("选项:%s 不存在 请检查拼写\n", argName) - } - os.Args = append(os.Args, arg) - } - } - opt.hasDec = true - } - resetFlag() - flag.Parse() - opt.EnableSingleFlight = *singleFlight - opt.NullCache = *nullCache - opt.EnablePaging = *paging - opt.EnableNullCache = *nullCache != "" - opt.EnableBatch = (*batchSize != 0) && (*maxGroup != 0) - opt.BatchErrBreak = *batchErr == "break" - opt.Sync = *sync - opt.CheckNullCode = *checkNullCode - opt.GroupSize = *batchSize - opt.MaxGroup = *maxGroup - opt.CustomMethod = *customMethod - opt.CacheErrContinue = *cacheErr == "continue" - opt.StructName = *structName -} - -func processList(s *pkg.Source, list *ast.Field) (opt options) { - fset := s.Fset - src := s.Src - lines := strings.Split(src, "\n") - opt = options{name: list.Names[0].Name, Args: s.GetDef(interfaceName), importPackages: s.Packages(list)} - // get comment - line := fset.Position(list.Pos()).Line - 3 - if len(lines)-1 >= line { - comment := lines[line] - opt.Comment = pkg.RegexpReplace(`\s+//(?P.+)`, comment, "$name") - opt.Comment = strings.TrimSpace(opt.Comment) - } - // get options - line = fset.Position(list.Pos()).Line - 2 - comment := lines[line] - getOptions(&opt, comment) - if !opt.hasDec { - log.Printf("%s: 无声明 忽略此方法\n", opt.name) - return - } - // get func - params := list.Type.(*ast.FuncType).Params.List - if len(params) == 0 { - log.Fatalln(opt.name + "参数不足") - } - for _, p := range params { - if len(p.Names) > 1 { - log.Fatalln(opt.name + "不支持省略类型 请写全声明中的字段类型名称") - } - } - if s.ExprString(params[0].Type) != "context.Context" { - log.Fatalln("第一个参数必须为context") - } - if len(params) == 1 { - opt.template = _noneTpl - } else { - opt.IDName = params[1].Names[0].Name - if _, ok := params[1].Type.(*ast.ArrayType); ok { - opt.template = _multiTpl - } else { - opt.template = _singleTpl - // get key - opt.keyType = s.ExprString(params[1].Type) - } - } - if len(params) > 2 { - var args []string - var allArgs []string - for _, pa := range params[2:] { - paType := s.ExprString(pa.Type) - if len(pa.Names) == 0 { - args = append(args, paType) - allArgs = append(allArgs, paType) - continue - } - var names []string - for _, name := range pa.Names { - names = append(names, name.Name) - } - allArgs = append(allArgs, strings.Join(names, ",")+" "+paType) - args = append(args, names...) - } - opt.ExtraArgs = strings.Join(args, ",") - opt.ExtraArgsType = strings.Join(allArgs, ",") - argsMap := make(map[string]bool) - for _, arg := range args { - argsMap[arg] = true - } - ignoreCache := make(map[string]bool) - ignoreRaw := make(map[string]bool) - ignoreAddCache := make(map[string]bool) - ignoreArray := [3]map[string]bool{ignoreCache, ignoreRaw, ignoreAddCache} - if *ignores != "" { - is := strings.Split(*ignores, "|") - if len(is) > 3 { - log.Fatalln("ignores参数错误") - } - for i := range is { - if len(is) > i { - for _, s := range strings.Split(is[i], ",") { - ignoreArray[i][s] = true - } - } - } - } - var as []string - for _, arg := range args { - if !ignoreCache[arg] { - as = append(as, arg) - } - } - opt.ExtraCacheArgs = strings.Join(as, ",") - as = []string{} - for _, arg := range args { - if !ignoreRaw[arg] { - as = append(as, arg) - } - } - opt.ExtraRawArgs = strings.Join(as, ",") - as = []string{} - for _, arg := range args { - if !ignoreAddCache[arg] { - as = append(as, arg) - } - } - opt.ExtraAddCacheArgs = strings.Join(as, ",") - if opt.ExtraAddCacheArgs != "" { - opt.ExtraAddCacheArgs = "," + opt.ExtraAddCacheArgs - } - if opt.ExtraRawArgs != "" { - opt.ExtraRawArgs = "," + opt.ExtraRawArgs - } - if opt.ExtraCacheArgs != "" { - opt.ExtraCacheArgs = "," + opt.ExtraCacheArgs - } - if opt.ExtraArgs != "" { - opt.ExtraArgs = "," + opt.ExtraArgs - } - if opt.ExtraArgsType != "" { - opt.ExtraArgsType = "," + opt.ExtraArgsType - } - } - // get k v from results - results := list.Type.(*ast.FuncType).Results.List - if len(results) != 2 { - log.Fatalln(opt.name + ": 参数个数不对") - } - if s.ExprString(results[1].Type) != "error" { - log.Fatalln(opt.name + ": 最后返回值参数需为error") - } - if opt.template == _multiTpl { - p, ok := results[0].Type.(*ast.MapType) - if !ok { - log.Fatalln(opt.name + ": 批量获取方法 返回值类型需为map类型") - } - opt.keyType = s.ExprString(p.Key) - opt.valueType = s.ExprString(p.Value) - } else { - opt.valueType = s.ExprString(results[0].Type) - } - for _, t := range numberTypes { - if t == opt.valueType { - opt.NumberValue = true - break - } - } - opt.ZeroValue = "nil" - for _, t := range simpleTypes { - if t == opt.valueType { - opt.SimpleValue = true - opt.ZeroValue = zeroValue(t) - break - } - } - if !opt.SimpleValue { - for _, t := range []string{"[]", "map"} { - if strings.HasPrefix(opt.valueType, t) { - opt.GoValue = true - break - } - } - } - upperName := strings.ToUpper(opt.name[0:1]) + opt.name[1:] - opt.cacheFunc = fmt.Sprintf("d.Cache%s", upperName) - opt.rawFunc = fmt.Sprintf("d.Raw%s", upperName) - opt.addCacheFunc = fmt.Sprintf("d.AddCache%s", upperName) - if opt.CustomMethod != "" { - arrs := strings.Split(opt.CustomMethod, "|") - if len(arrs) > 0 && arrs[0] != "" { - opt.cacheFunc = arrs[0] - } - if len(arrs) > 1 && arrs[1] != "" { - opt.rawFunc = arrs[1] - } - if len(arrs) > 2 && arrs[2] != "" { - opt.addCacheFunc = arrs[2] - } - } - return -} - -// parse parse options -func parse(s *pkg.Source) (opts []*options) { - var c *ast.Object - for _, name := range []string{"_bts", "Dao"} { - c = s.F.Scope.Lookup(name) - if (c == nil) || (c.Kind != ast.Typ) { - c = nil - continue - } - interfaceName = name - break - } - if c == nil { - log.Fatalln("无法找到缓存声明") - } - lists := c.Decl.(*ast.TypeSpec).Type.(*ast.InterfaceType).Methods.List - for _, list := range lists { - opt := processList(s, list) - if opt.hasDec { - opt.Check() - opts = append(opts, &opt) - } - } - return -} - -func (option *options) Check() { - if !option.SimpleValue && !strings.Contains(option.valueType, "*") && !strings.Contains(option.valueType, "[]") && !strings.Contains(option.valueType, "map") { - log.Fatalf("%s: 值类型只能为基本类型/slice/map/指针类型\n", option.name) - } - if option.EnableSingleFlight && option.EnableBatch { - log.Fatalf("%s: 单飞和批量获取不能同时开启\n", option.name) - } - if option.template != _singleTpl && option.EnablePaging { - log.Fatalf("%s: 分页只能用在单key模板中\n", option.name) - } - if option.SimpleValue && !option.EnableNullCache { - if !((option.template == _multiTpl) && option.NumberValue) { - log.Fatalf("%s: 值为基本类型时需开启空缓存 防止缓存零值穿透\n", option.name) - } - } - if option.EnableNullCache { - if !option.SimpleValue && option.CheckNullCode == "" { - log.Fatalf("%s: 缺少-check_null_code参数\n", option.name) - } - if option.SimpleValue && option.NullCache == option.ZeroValue { - log.Fatalf("%s: %s 不能作为空缓存值 \n", option.name, option.NullCache) - } - if strings.Contains(option.CheckNullCode, "len") && strings.Contains(strings.Replace(option.CheckNullCode, " ", "", -1), "==0") { - // -check_null_code=len($)==0 这种无效 - log.Fatalf("%s: -check_null_code=%s 错误 会有无意义的赋值\n", option.name, option.CheckNullCode) - } - } -} - -func genHeader(opts []*options) (src string) { - option := options{PkgName: os.Getenv("GOPACKAGE")} - option.UseBTS = interfaceName == "_bts" - var sfCount int - var packages, sfInit []string - packagesMap := map[string]bool{`"context"`: true} - for _, opt := range opts { - if opt.EnableSingleFlight { - option.EnableSingleFlight = true - sfCount++ - } - if opt.EnableBatch { - option.EnableBatch = true - } - if len(opt.importPackages) > 0 { - for _, pkg := range opt.importPackages { - if !packagesMap[pkg] { - packages = append(packages, pkg) - packagesMap[pkg] = true - } - } - } - if opt.Args != "" { - option.Args = opt.Args - } - } - option.ImportPackage = strings.Join(packages, "\n") - for i := 0; i < sfCount; i++ { - sfInit = append(sfInit, "{}") - } - src = _headerTemplate - src = strings.Replace(src, "SFCOUNT", strconv.Itoa(sfCount), -1) - t := template.Must(template.New("header").Parse(src)) - var buffer bytes.Buffer - err := t.Execute(&buffer, option) - if err != nil { - log.Fatalf("execute template: %s", err) - } - // Format the output. - src = strings.Replace(buffer.String(), "\t", "", -1) - src = regexp.MustCompile("\n+").ReplaceAllString(src, "\n") - src = strings.Replace(src, "NEWLINE", "", -1) - src = strings.Replace(src, "ARGS", option.Args, -1) - src = strings.Replace(src, "SFINIT", strings.Join(sfInit, ","), -1) - return -} - -func genBody(opts []*options) (res string) { - sfnum := -1 - for _, option := range opts { - var nullCodeVar, src string - if option.template == _multiTpl { - src = _multiTemplate - nullCodeVar = "v" - } else if option.template == _singleTpl { - src = _singleTemplate - nullCodeVar = "res" - } else { - src = _noneTemplate - nullCodeVar = "res" - } - if option.template != _noneTpl { - src = strings.Replace(src, "KEY", option.keyType, -1) - } - if option.CheckNullCode != "" { - option.CheckNullCode = strings.Replace(option.CheckNullCode, "$", nullCodeVar, -1) - } - if option.EnableSingleFlight { - sfnum++ - } - src = strings.Replace(src, "NAME", option.name, -1) - src = strings.Replace(src, "VALUE", option.valueType, -1) - src = strings.Replace(src, "ADDCACHEFUNC", option.addCacheFunc, -1) - src = strings.Replace(src, "CACHEFUNC", option.cacheFunc, -1) - src = strings.Replace(src, "RAWFUNC", option.rawFunc, -1) - src = strings.Replace(src, "GROUPSIZE", strconv.Itoa(option.GroupSize), -1) - src = strings.Replace(src, "MAXGROUP", strconv.Itoa(option.MaxGroup), -1) - src = strings.Replace(src, "SFNUM", strconv.Itoa(sfnum), -1) - t := template.Must(template.New("cache").Parse(src)) - var buffer bytes.Buffer - err := t.Execute(&buffer, option) - if err != nil { - log.Fatalf("execute template: %s", err) - } - // Format the output. - src = strings.Replace(buffer.String(), "\t", "", -1) - src = regexp.MustCompile("\n+").ReplaceAllString(src, "\n") - res = res + "\n" + src - } - return -} - -func zeroValue(t string) string { - switch t { - case "bool": - return "false" - case "string": - return "\"\"" - case "[]byte": - return "nil" - default: - return "0" - } -} - -func init() { - for _, name := range optionNames { - optionNamesMap[name] = true - } -} - -func main() { - log.SetFlags(0) - defer func() { - if err := recover(); err != nil { - buf := make([]byte, 64*1024) - buf = buf[:runtime.Stack(buf, false)] - log.Fatalf("程序解析失败, err: %+v stack: %s", err, buf) - } - }() - options := parse(pkg.NewSource(pkg.SourceText())) - header := genHeader(options) - body := genBody(options) - code := pkg.FormatCode(header + "\n" + body) - // Write to file. - dir := filepath.Dir(".") - outputName := filepath.Join(dir, "dao.bts.go") - err := ioutil.WriteFile(outputName, []byte(code), 0644) - if err != nil { - log.Fatalf("写入文件失败: %s", err) - } - log.Println("dao.bts.go: 生成成功") -} diff --git a/tool/kratos-gen-bts/multi_template.go b/tool/kratos-gen-bts/multi_template.go deleted file mode 100644 index 7034f00af..000000000 --- a/tool/kratos-gen-bts/multi_template.go +++ /dev/null @@ -1,130 +0,0 @@ -package main - -var _multiTemplate = ` -// NAME {{or .Comment "get data from cache if miss will call source method, then add to cache."}} -func (d *{{.StructName}}) NAME(c context.Context, {{.IDName}} []KEY{{.ExtraArgsType}}) (res map[KEY]VALUE, err error) { - if len({{.IDName}}) == 0 { - return - } - addCache := true - if res, err = CACHEFUNC(c, {{.IDName}} {{.ExtraCacheArgs}});err != nil { - {{if .CacheErrContinue}} - addCache = false - res = nil - err = nil - {{else}} - return - {{end}} - } - var miss []KEY - for _, key := range {{.IDName}} { - {{if .GoValue}} - if (res == nil) || (len(res[key]) == 0) { - {{else}} - {{if .NumberValue}} - if _, ok := res[key]; !ok { - {{else}} - if (res == nil) || (res[key] == {{.ZeroValue}}) { - {{end}} - {{end}} - miss = append(miss, key) - } - } - cache.MetricHits.Add(float64(len({{.IDName}}) - len(miss)), "bts:NAME") - {{if .EnableNullCache}} - for k, v := range res { - {{if .SimpleValue}} if v == {{.NullCache}} { {{else}} if {{.CheckNullCode}} { {{end}} - delete(res, k) - } - } - {{end}} - missLen := len(miss) - if missLen == 0 { - return - } - {{if .EnableBatch}} - missData := make(map[KEY]VALUE, missLen) - {{else}} - var missData map[KEY]VALUE - {{end}} - {{if .EnableSingleFlight}} - var rr interface{} - sf := d.cacheSFNAME({{.IDName}} {{.ExtraArgs}}) - rr, err, _ = cacheSingleFlights[SFNUM].Do(sf, func() (r interface{}, e error) { - cache.MetricMisses.Add(float64(len(miss)), "bts:NAME") - r, e = RAWFUNC(c, miss {{.ExtraRawArgs}}) - return - }) - missData = rr.(map[KEY]VALUE) - {{else}} - {{if .EnableBatch}} - cache.MetricMisses.Add(float64(missLen), "bts:NAME") - var mutex sync.Mutex - {{if .BatchErrBreak}} - group := errgroup.WithCancel(c) - {{else}} - group := errgroup.WithContext(c) - {{end}} - if missLen > MAXGROUP { - group.GOMAXPROCS(MAXGROUP) - } - var run = func(ms []KEY) { - group.Go(func(ctx context.Context) (err error) { - data, err := RAWFUNC(ctx, ms {{.ExtraRawArgs}}) - mutex.Lock() - for k, v := range data { - missData[k] = v - } - mutex.Unlock() - return - }) - } - var ( - i int - n = missLen/GROUPSIZE - ) - for i=0; i< n; i++{ - run(miss[i*GROUPSIZE:(i+1)*GROUPSIZE]) - } - if len(miss[i*GROUPSIZE:]) > 0 { - run(miss[i*GROUPSIZE:]) - } - err = group.Wait() - {{else}} - cache.MetricMisses.Add(float64(len(miss)), "bts:NAME") - missData, err = RAWFUNC(c, miss {{.ExtraRawArgs}}) - {{end}} - {{end}} - if res == nil { - res = make(map[KEY]VALUE, len({{.IDName}})) - } - for k, v := range missData { - res[k] = v - } - if err != nil { - return - } - {{if .EnableNullCache}} - for _, key := range miss { - {{if .GoValue}} - if len(res[key]) == 0 { - {{else}} - if res[key] == {{.ZeroValue}} { - {{end}} - missData[key] = {{.NullCache}} - } - } - {{end}} - if !addCache { - return - } - {{if .Sync}} - ADDCACHEFUNC(c, missData {{.ExtraAddCacheArgs}}) - {{else}} - d.cache.Do(c, func(c context.Context) { - ADDCACHEFUNC(c, missData {{.ExtraAddCacheArgs}}) - }) - {{end}} - return -} -` diff --git a/tool/kratos-gen-bts/none_template.go b/tool/kratos-gen-bts/none_template.go deleted file mode 100644 index 90c4063d0..000000000 --- a/tool/kratos-gen-bts/none_template.go +++ /dev/null @@ -1,69 +0,0 @@ -package main - -var _noneTemplate = ` -// NAME {{or .Comment "get data from cache if miss will call source method, then add to cache."}} -func (d *{{.StructName}}) NAME(c context.Context) (res VALUE, err error) { - addCache := true - res, err = CACHEFUNC(c) - if err != nil { - {{if .CacheErrContinue}} - addCache = false - err = nil - {{else}} - return - {{end}} - } - {{if .EnableNullCache}} - defer func() { - {{if .SimpleValue}} if res == {{.NullCache}} { {{else}} if {{.CheckNullCode}} { {{end}} - res = {{.ZeroValue}} - } - }() - {{end}} - {{if .GoValue}} - if len(res) != 0 { - {{else}} - if res != {{.ZeroValue}} { - {{end}} - cache.MetricHits.Inc("bts:NAME") - return - } - {{if .EnableSingleFlight}} - var rr interface{} - sf := d.cacheSFNAME() - rr, err, _ = cacheSingleFlights[SFNUM].Do(sf, func() (r interface{}, e error) { - cache.MetricMisses.Inc("bts:NAME") - r, e = RAWFUNC(c) - return - }) - res = rr.(VALUE) - {{else}} - cache.MetricMisses.Inc("bts:NAME") - res, err = RAWFUNC(c) - {{end}} - if err != nil { - return - } - var miss = res - {{if .EnableNullCache}} - {{if .GoValue}} - if len(miss) == 0 { - {{else}} - if miss == {{.ZeroValue}} { - {{end}} - miss = {{.NullCache}} - } - {{end}} - if !addCache { - return - } - {{if .Sync}} - ADDCACHEFUNC(c, miss) - {{else}} - d.cache.Do(c, func(c context.Context) { - ADDCACHEFUNC(c, miss) - }) - {{end}} - return -} -` diff --git a/tool/kratos-gen-bts/single_template.go b/tool/kratos-gen-bts/single_template.go deleted file mode 100644 index 0ec2d4473..000000000 --- a/tool/kratos-gen-bts/single_template.go +++ /dev/null @@ -1,90 +0,0 @@ -package main - -var _singleTemplate = ` -// NAME {{or .Comment "get data from cache if miss will call source method, then add to cache."}} -func (d *{{.StructName}}) NAME(c context.Context, {{.IDName}} KEY{{.ExtraArgsType}}) (res VALUE, err error) { - addCache := true - res, err = CACHEFUNC(c, {{.IDName}} {{.ExtraCacheArgs}}) - if err != nil { - {{if .CacheErrContinue}} - addCache = false - err = nil - {{else}} - return - {{end}} - } - {{if .EnableNullCache}} - defer func() { - {{if .SimpleValue}} if res == {{.NullCache}} { {{else}} if {{.CheckNullCode}} { {{end}} - res = {{.ZeroValue}} - } - }() - {{end}} - {{if .GoValue}} - if len(res) != 0 { - {{else}} - if res != {{.ZeroValue}} { - {{end}} - cache.MetricHits.Inc("bts:NAME") - return - } - {{if .EnablePaging}} - var miss VALUE - {{end}} - {{if .EnableSingleFlight}} - var rr interface{} - sf := d.cacheSFNAME({{.IDName}} {{.ExtraArgs}}) - rr, err, _ = cacheSingleFlights[SFNUM].Do(sf, func() (r interface{}, e error) { - cache.MetricMisses.Inc("bts:NAME") - {{if .EnablePaging}} - var rrs [2]interface{} - rrs[0], rrs[1], e = RAWFUNC(c, {{.IDName}} {{.ExtraRawArgs}}) - r = rrs - {{else}} - r, e = RAWFUNC(c, {{.IDName}} {{.ExtraRawArgs}}) - {{end}} - return - }) - {{if .EnablePaging}} - res = rr.([2]interface{})[0].(VALUE) - miss = rr.([2]interface{})[1].(VALUE) - {{else}} - res = rr.(VALUE) - {{end}} - {{else}} - cache.MetricMisses.Inc("bts:NAME") - {{if .EnablePaging}} - res, miss, err = RAWFUNC(c, {{.IDName}} {{.ExtraRawArgs}}) - {{else}} - res, err = RAWFUNC(c, {{.IDName}} {{.ExtraRawArgs}}) - {{end}} - {{end}} - if err != nil { - return - } - {{if .EnablePaging}} - {{else}} - miss := res - {{end}} - {{if .EnableNullCache}} - {{if .GoValue}} - if len(miss) == 0 { - {{else}} - if miss == {{.ZeroValue}} { - {{end}} - miss = {{.NullCache}} - } - {{end}} - if !addCache { - return - } - {{if .Sync}} - ADDCACHEFUNC(c, {{.IDName}}, miss {{.ExtraAddCacheArgs}}) - {{else}} - d.cache.Do(c, func(c context.Context) { - ADDCACHEFUNC(c, {{.IDName}}, miss {{.ExtraAddCacheArgs}}) - }) - {{end}} - return -} -` diff --git a/tool/kratos-gen-bts/testdata/dao.bts.go b/tool/kratos-gen-bts/testdata/dao.bts.go deleted file mode 100644 index cba448576..000000000 --- a/tool/kratos-gen-bts/testdata/dao.bts.go +++ /dev/null @@ -1,283 +0,0 @@ -// Code generated by kratos tool genbts. DO NOT EDIT. - -/* - Package testdata is a generated cache proxy package. - It is generated from: - type _bts interface { - // bts: -batch=2 -max_group=20 -batch_err=break -nullcache=&Demo{ID:-1} -check_null_code=$.ID==-1 - Demos(c context.Context, keys []int64) (map[int64]*Demo, error) - // bts: -batch=2 -max_group=20 -batch_err=continue -nullcache=&Demo{ID:-1} -check_null_code=$.ID==-1 - Demos1(c context.Context, keys []int64) (map[int64]*Demo, error) - // bts: -sync=true -nullcache=&Demo{ID:-1} -check_null_code=$.ID==-1 - Demo(c context.Context, key int64) (*Demo, error) - // bts: -paging=true - Demo1(c context.Context, key int64, pn int, ps int) (*Demo, error) - // bts: -nullcache=&Demo{ID:-1} -check_null_code=$.ID==-1 - None(c context.Context) (*Demo, error) - } -*/ - -package testdata - -import ( - "context" - "sync" - - "github.com/go-kratos/kratos/pkg/cache" - "github.com/go-kratos/kratos/pkg/sync/errgroup" -) - -var _ _bts - -// Demos get data from cache if miss will call source method, then add to cache. -func (d *dao) Demos(c context.Context, keys []int64) (res map[int64]*Demo, err error) { - if len(keys) == 0 { - return - } - addCache := true - if res, err = d.CacheDemos(c, keys); err != nil { - addCache = false - res = nil - err = nil - } - var miss []int64 - for _, key := range keys { - if (res == nil) || (res[key] == nil) { - miss = append(miss, key) - } - } - cache.MetricHits.Add(float64(len(keys)-len(miss)), "bts:Demos") - for k, v := range res { - if v.ID == -1 { - delete(res, k) - } - } - missLen := len(miss) - if missLen == 0 { - return - } - missData := make(map[int64]*Demo, missLen) - cache.MetricMisses.Add(float64(missLen), "bts:Demos") - var mutex sync.Mutex - group := errgroup.WithCancel(c) - if missLen > 20 { - group.GOMAXPROCS(20) - } - var run = func(ms []int64) { - group.Go(func(ctx context.Context) (err error) { - data, err := d.RawDemos(ctx, ms) - mutex.Lock() - for k, v := range data { - missData[k] = v - } - mutex.Unlock() - return - }) - } - var ( - i int - n = missLen / 2 - ) - for i = 0; i < n; i++ { - run(miss[i*2 : (i+1)*2]) - } - if len(miss[i*2:]) > 0 { - run(miss[i*2:]) - } - err = group.Wait() - if res == nil { - res = make(map[int64]*Demo, len(keys)) - } - for k, v := range missData { - res[k] = v - } - if err != nil { - return - } - for _, key := range miss { - if res[key] == nil { - missData[key] = &Demo{ID: -1} - } - } - if !addCache { - return - } - d.cache.Do(c, func(c context.Context) { - d.AddCacheDemos(c, missData) - }) - return -} - -// Demos1 get data from cache if miss will call source method, then add to cache. -func (d *dao) Demos1(c context.Context, keys []int64) (res map[int64]*Demo, err error) { - if len(keys) == 0 { - return - } - addCache := true - if res, err = d.CacheDemos1(c, keys); err != nil { - addCache = false - res = nil - err = nil - } - var miss []int64 - for _, key := range keys { - if (res == nil) || (res[key] == nil) { - miss = append(miss, key) - } - } - cache.MetricHits.Add(float64(len(keys)-len(miss)), "bts:Demos1") - for k, v := range res { - if v.ID == -1 { - delete(res, k) - } - } - missLen := len(miss) - if missLen == 0 { - return - } - missData := make(map[int64]*Demo, missLen) - cache.MetricMisses.Add(float64(missLen), "bts:Demos1") - var mutex sync.Mutex - group := errgroup.WithContext(c) - if missLen > 20 { - group.GOMAXPROCS(20) - } - var run = func(ms []int64) { - group.Go(func(ctx context.Context) (err error) { - data, err := d.RawDemos1(ctx, ms) - mutex.Lock() - for k, v := range data { - missData[k] = v - } - mutex.Unlock() - return - }) - } - var ( - i int - n = missLen / 2 - ) - for i = 0; i < n; i++ { - run(miss[i*2 : (i+1)*2]) - } - if len(miss[i*2:]) > 0 { - run(miss[i*2:]) - } - err = group.Wait() - if res == nil { - res = make(map[int64]*Demo, len(keys)) - } - for k, v := range missData { - res[k] = v - } - if err != nil { - return - } - for _, key := range miss { - if res[key] == nil { - missData[key] = &Demo{ID: -1} - } - } - if !addCache { - return - } - d.cache.Do(c, func(c context.Context) { - d.AddCacheDemos1(c, missData) - }) - return -} - -// Demo get data from cache if miss will call source method, then add to cache. -func (d *dao) Demo(c context.Context, key int64) (res *Demo, err error) { - addCache := true - res, err = d.CacheDemo(c, key) - if err != nil { - addCache = false - err = nil - } - defer func() { - if res.ID == -1 { - res = nil - } - }() - if res != nil { - cache.MetricHits.Inc("bts:Demo") - return - } - cache.MetricMisses.Inc("bts:Demo") - res, err = d.RawDemo(c, key) - if err != nil { - return - } - miss := res - if miss == nil { - miss = &Demo{ID: -1} - } - if !addCache { - return - } - d.AddCacheDemo(c, key, miss) - return -} - -// Demo1 get data from cache if miss will call source method, then add to cache. -func (d *dao) Demo1(c context.Context, key int64, pn int, ps int) (res *Demo, err error) { - addCache := true - res, err = d.CacheDemo1(c, key, pn, ps) - if err != nil { - addCache = false - err = nil - } - if res != nil { - cache.MetricHits.Inc("bts:Demo1") - return - } - var miss *Demo - cache.MetricMisses.Inc("bts:Demo1") - res, miss, err = d.RawDemo1(c, key, pn, ps) - if err != nil { - return - } - if !addCache { - return - } - d.cache.Do(c, func(c context.Context) { - d.AddCacheDemo1(c, key, miss, pn, ps) - }) - return -} - -// None get data from cache if miss will call source method, then add to cache. -func (d *dao) None(c context.Context) (res *Demo, err error) { - addCache := true - res, err = d.CacheNone(c) - if err != nil { - addCache = false - err = nil - } - defer func() { - if res.ID == -1 { - res = nil - } - }() - if res != nil { - cache.MetricHits.Inc("bts:None") - return - } - cache.MetricMisses.Inc("bts:None") - res, err = d.RawNone(c) - if err != nil { - return - } - var miss = res - if miss == nil { - miss = &Demo{ID: -1} - } - if !addCache { - return - } - d.cache.Do(c, func(c context.Context) { - d.AddCacheNone(c, miss) - }) - return -} diff --git a/tool/kratos-gen-bts/testdata/dao.go b/tool/kratos-gen-bts/testdata/dao.go deleted file mode 100644 index 1958f8424..000000000 --- a/tool/kratos-gen-bts/testdata/dao.go +++ /dev/null @@ -1,37 +0,0 @@ -package testdata - -import ( - "context" - - "github.com/go-kratos/kratos/pkg/sync/pipeline/fanout" -) - -// Demo test struct -type Demo struct { - ID int64 - Title string -} - -// Dao . -type dao struct { - cache *fanout.Fanout -} - -// New . -func New() *dao { - return &dao{cache: fanout.New("cache")} -} - -//go:generate kratos tool genbts -type _bts interface { - // bts: -batch=2 -max_group=20 -batch_err=break -nullcache=&Demo{ID:-1} -check_null_code=$.ID==-1 - Demos(c context.Context, keys []int64) (map[int64]*Demo, error) - // bts: -batch=2 -max_group=20 -batch_err=continue -nullcache=&Demo{ID:-1} -check_null_code=$.ID==-1 - Demos1(c context.Context, keys []int64) (map[int64]*Demo, error) - // bts: -sync=true -nullcache=&Demo{ID:-1} -check_null_code=$.ID==-1 - Demo(c context.Context, key int64) (*Demo, error) - // bts: -paging=true - Demo1(c context.Context, key int64, pn int, ps int) (*Demo, error) - // bts: -nullcache=&Demo{ID:-1} -check_null_code=$.ID==-1 - None(c context.Context) (*Demo, error) -} diff --git a/tool/kratos-gen-bts/testdata/multi.go b/tool/kratos-gen-bts/testdata/multi.go deleted file mode 100644 index 4685d37ef..000000000 --- a/tool/kratos-gen-bts/testdata/multi.go +++ /dev/null @@ -1,48 +0,0 @@ -package testdata - -import ( - "context" -) - -// mock test -var ( - _multiCacheFunc func(c context.Context, keys []int64) (map[int64]*Demo, error) - _multiRawFunc func(c context.Context, keys []int64) (map[int64]*Demo, error) - _multiAddCacheFunc func(c context.Context, values map[int64]*Demo) error -) - -// CacheDemos . -func (d *dao) CacheDemos(c context.Context, keys []int64) (map[int64]*Demo, error) { - // get data from cache - return _multiCacheFunc(c, keys) -} - -// RawDemos . -func (d *dao) RawDemos(c context.Context, keys []int64) (map[int64]*Demo, error) { - // get data from db - return _multiRawFunc(c, keys) -} - -// AddCacheDemos . -func (d *dao) AddCacheDemos(c context.Context, values map[int64]*Demo) error { - // add to cache - return _multiAddCacheFunc(c, values) -} - -// CacheDemos1 . -func (d *dao) CacheDemos1(c context.Context, keys []int64) (map[int64]*Demo, error) { - // get data from cache - return _multiCacheFunc(c, keys) -} - -// RawDemos . -func (d *dao) RawDemos1(c context.Context, keys []int64) (map[int64]*Demo, error) { - // get data from db - return _multiRawFunc(c, keys) -} - -// AddCacheDemos . -func (d *dao) AddCacheDemos1(c context.Context, values map[int64]*Demo) error { - // add to cache - return _multiAddCacheFunc(c, values) -} diff --git a/tool/kratos-gen-bts/testdata/multi_test.go b/tool/kratos-gen-bts/testdata/multi_test.go deleted file mode 100644 index 78319acd3..000000000 --- a/tool/kratos-gen-bts/testdata/multi_test.go +++ /dev/null @@ -1,67 +0,0 @@ -package testdata - -import ( - "context" - "errors" - "testing" -) - -func TestMultiCache(t *testing.T) { - id := int64(1) - d := New() - meta := map[int64]*Demo{id: {ID: id}} - getsFromCache := func(c context.Context, keys []int64) (map[int64]*Demo, error) { return meta, nil } - notGetsFromCache := func(c context.Context, keys []int64) (map[int64]*Demo, error) { return nil, errors.New("err") } - // 缓存返回了部分数据 - partFromCache := func(c context.Context, keys []int64) (map[int64]*Demo, error) { return meta, errors.New("err") } - getsFromSource := func(c context.Context, keys []int64) (map[int64]*Demo, error) { return meta, nil } - notGetsFromSource := func(c context.Context, keys []int64) (map[int64]*Demo, error) { - return meta, errors.New("err") - } - addToCache := func(c context.Context, values map[int64]*Demo) error { return nil } - // gets from cache - _multiCacheFunc = getsFromCache - _multiRawFunc = notGetsFromSource - _multiAddCacheFunc = addToCache - res, err := d.Demos(context.TODO(), []int64{id}) - if err != nil { - t.Fatalf("err should be nil, get: %v", err) - } - if res[1].ID != 1 { - t.Fatalf("id should be 1") - } - // get from source - _multiCacheFunc = notGetsFromCache - _multiRawFunc = getsFromSource - res, err = d.Demos(context.TODO(), []int64{1, 2, 3, 4, 5, 6}) - if err != nil { - t.Fatalf("err should be nil, get: %v", err) - } - if res[1].ID != 1 { - t.Fatalf("id should be 1") - } - // 缓存失败 返回部分数据 回源也失败的情况 - _multiCacheFunc = partFromCache - _multiRawFunc = notGetsFromSource - res, err = d.Demos(context.TODO(), []int64{id}) - if err == nil { - t.Fatalf("err should be nil, get: %v", err) - } - if res[1].ID != 1 { - t.Fatalf("id should be 1") - } - // with null cache - nullCache := &Demo{ID: -1} - getNullFromCache := func(c context.Context, keys []int64) (map[int64]*Demo, error) { - return map[int64]*Demo{id: nullCache}, nil - } - _multiCacheFunc = getNullFromCache - _multiRawFunc = notGetsFromSource - res, err = d.Demos(context.TODO(), []int64{id}) - if err != nil { - t.Fatalf("err should be nil, get: %v", err) - } - if res[id] != nil { - t.Fatalf("res should be nil") - } -} diff --git a/tool/kratos-gen-bts/testdata/none.go b/tool/kratos-gen-bts/testdata/none.go deleted file mode 100644 index c3202613a..000000000 --- a/tool/kratos-gen-bts/testdata/none.go +++ /dev/null @@ -1,30 +0,0 @@ -package testdata - -import ( - "context" -) - -// mock test -var ( - _noneCacheFunc func(c context.Context) (*Demo, error) - _noneRawFunc func(c context.Context) (*Demo, error) - _noneAddCacheFunc func(c context.Context, value *Demo) error -) - -// CacheNone . -func (d *dao) CacheNone(c context.Context) (*Demo, error) { - // get data from cache - return _noneCacheFunc(c) -} - -// RawNone . -func (d *dao) RawNone(c context.Context) (*Demo, error) { - // get data from db - return _noneRawFunc(c) -} - -// AddCacheNone . -func (d *dao) AddCacheNone(c context.Context, value *Demo) error { - // add to cache - return _noneAddCacheFunc(c, value) -} diff --git a/tool/kratos-gen-bts/testdata/none_test.go b/tool/kratos-gen-bts/testdata/none_test.go deleted file mode 100644 index 6ace25d75..000000000 --- a/tool/kratos-gen-bts/testdata/none_test.go +++ /dev/null @@ -1,50 +0,0 @@ -package testdata - -import ( - "context" - "errors" - "testing" -) - -func TestNoneCache(t *testing.T) { - d := New() - meta := &Demo{ID: 1} - getFromCache := func(c context.Context) (*Demo, error) { return meta, nil } - notGetFromCache := func(c context.Context) (*Demo, error) { return nil, errors.New("err") } - getFromSource := func(c context.Context) (*Demo, error) { return meta, nil } - notGetFromSource := func(c context.Context) (*Demo, error) { return meta, errors.New("err") } - addToCache := func(c context.Context, values *Demo) error { return nil } - // get from cache - _noneCacheFunc = getFromCache - _noneRawFunc = notGetFromSource - _noneAddCacheFunc = addToCache - res, err := d.None(context.TODO()) - if err != nil { - t.Fatalf("err should be nil, get: %v", err) - } - if res.ID != 1 { - t.Fatalf("id should be 1") - } - // get from source - _noneCacheFunc = notGetFromCache - _noneRawFunc = getFromSource - res, err = d.None(context.TODO()) - if err != nil { - t.Fatalf("err should be nil, get: %v", err) - } - if res.ID != 1 { - t.Fatalf("id should be 1") - } - // with null cache - nullCache := &Demo{ID: -1} - getNullFromCache := func(c context.Context) (*Demo, error) { return nullCache, nil } - _noneCacheFunc = getNullFromCache - _noneRawFunc = notGetFromSource - res, err = d.None(context.TODO()) - if err != nil { - t.Fatalf("err should be nil, get: %v", err) - } - if res != nil { - t.Fatalf("res should be nil") - } -} diff --git a/tool/kratos-gen-bts/testdata/single.go b/tool/kratos-gen-bts/testdata/single.go deleted file mode 100644 index fe390216a..000000000 --- a/tool/kratos-gen-bts/testdata/single.go +++ /dev/null @@ -1,48 +0,0 @@ -package testdata - -import ( - "context" -) - -// mock test -var ( - _singleCacheFunc func(c context.Context, key int64) (*Demo, error) - _singleRawFunc func(c context.Context, key int64) (*Demo, error) - _singleAddCacheFunc func(c context.Context, key int64, value *Demo) error -) - -// CacheDemo . -func (d *dao) CacheDemo(c context.Context, key int64) (*Demo, error) { - // get data from cache - return _singleCacheFunc(c, key) -} - -// RawDemo . -func (d *dao) RawDemo(c context.Context, key int64) (*Demo, error) { - // get data from db - return _singleRawFunc(c, key) -} - -// AddCacheDemo . -func (d *dao) AddCacheDemo(c context.Context, key int64, value *Demo) error { - // add to cache - return _singleAddCacheFunc(c, key, value) -} - -// CacheDemo1 . -func (d *dao) CacheDemo1(c context.Context, key int64, pn, ps int) (*Demo, error) { - // get data from cache - return nil, nil -} - -// RawDemo1 . -func (d *dao) RawDemo1(c context.Context, key int64, pn, ps int) (*Demo, *Demo, error) { - // get data from db - return nil, nil, nil -} - -// AddCacheDemo1 . -func (d *dao) AddCacheDemo1(c context.Context, key int64, value *Demo, pn, ps int) error { - // add to cache - return nil -} diff --git a/tool/kratos-gen-bts/testdata/single_test.go b/tool/kratos-gen-bts/testdata/single_test.go deleted file mode 100644 index e74973d2c..000000000 --- a/tool/kratos-gen-bts/testdata/single_test.go +++ /dev/null @@ -1,50 +0,0 @@ -package testdata - -import ( - "context" - "errors" - "testing" -) - -func TestSingleCache(t *testing.T) { - d := New() - meta := &Demo{ID: 1} - getFromCache := func(c context.Context, id int64) (*Demo, error) { return meta, nil } - notGetFromCache := func(c context.Context, id int64) (*Demo, error) { return nil, errors.New("err") } - getFromSource := func(c context.Context, id int64) (*Demo, error) { return meta, nil } - notGetFromSource := func(c context.Context, id int64) (*Demo, error) { return meta, errors.New("err") } - addToCache := func(c context.Context, id int64, values *Demo) error { return nil } - // get from cache - _singleCacheFunc = getFromCache - _singleRawFunc = notGetFromSource - _singleAddCacheFunc = addToCache - res, err := d.Demo(context.TODO(), 1) - if err != nil { - t.Fatalf("err should be nil, get: %v", err) - } - if res.ID != 1 { - t.Fatalf("id should be 1") - } - // get from source - _singleCacheFunc = notGetFromCache - _singleRawFunc = getFromSource - res, err = d.Demo(context.TODO(), 1) - if err != nil { - t.Fatalf("err should be nil, get: %v", err) - } - if res.ID != 1 { - t.Fatalf("id should be 1") - } - // with null cache - nullCache := &Demo{ID: -1} - getNullFromCache := func(c context.Context, id int64) (*Demo, error) { return nullCache, nil } - _singleCacheFunc = getNullFromCache - _singleRawFunc = notGetFromSource - res, err = d.Demo(context.TODO(), 1) - if err != nil { - t.Fatalf("err should be nil, get: %v", err) - } - if res != nil { - t.Fatalf("res should be nil") - } -} diff --git a/tool/kratos-gen-mc/README.md b/tool/kratos-gen-mc/README.md deleted file mode 100644 index 0bc8c89ff..000000000 --- a/tool/kratos-gen-mc/README.md +++ /dev/null @@ -1,45 +0,0 @@ - -#### genmc - -> mc缓存代码生成 - -##### 项目简介 - -自动生成memcached缓存代码 和缓存回源工具kratos-gen-bts配合使用 体验更佳 -支持以下功能: -- 常用mc命令(get/set/add/replace/delete) -- 多种数据存储格式(json/pb/raw/gob/gzip) -- 常用值类型自动转换(int/bool/float...) -- 自定义缓存名称和过期时间 -- 记录pkg/error错误栈 -- 记录日志trace id -- prometheus错误监控 -- 自定义参数个数 -- 自定义注释 - -##### 使用方式: -1. dao.go文件中新增 _mc interface -2. 在dao 文件夹中执行 go generate命令 将会生成相应的缓存代码 -3. 示例见testdata/dao.go - -##### 注意: -类型会根据前缀进行猜测 -set / add 对应mc方法Set -replace 对应mc方法 Replace -del 对应mc方法 Delete -get / cache对应mc方法Get -mc Add方法需要用注解 -type=only_add单独指定 - -#### 注解参数: -| 名称 | 默认值 | 可用范围 | 说明 | 可选值 | 示例 | -| ----------- | ------------------- | ---------------- | ------------------------------------------------------------ | ---------------------------- | -------------------------- | -| encode | 根据值类型raw或json | set/add/replace | 数据存储的格式 | json/pb/raw/gob/gzip | json 或 json\|gzip 或gob等 | -| type | 前缀推断 | 全部 | mc方法 set/get/delete... | get/set/del/replace/only_add | get 或 replace 等 | -| key | 根据方法名称生成 | 全部 | 缓存key名称 | - | demoKey | -| expire | 根据方法名称生成 | 全部 | 缓存过期时间 | - | d.demoExpire | -| batch | | get(限多key模板) | 批量获取数据 每组大小 | - | 100 | -| max_group | | get(限多key模板) | 批量获取数据 最大组数量 | - | 10 | -| batch_err | break | get(限多key模板) | 批量获取数据回源错误的时候 降级继续请求(continue)还是直接返回(break) | break 或 continue | continue | -| struct_name | dao | 全部 | 用户自定义Dao结构体名称 | | MemcacheDao | -|check_null_code||add/set|(和null_expire配套使用)判断是否是空缓存的代码 用于为空缓存独立设定过期时间||$.ID==-1 或者 $=="-1"等| -|null_expire|300(5分钟)|add/set|(和check_null_code配套使用)空缓存的过期时间||d.nullExpire| \ No newline at end of file diff --git a/tool/kratos-gen-mc/header_template.go b/tool/kratos-gen-mc/header_template.go deleted file mode 100644 index 5c6cf763e..000000000 --- a/tool/kratos-gen-mc/header_template.go +++ /dev/null @@ -1,29 +0,0 @@ -package main - -var _headerTemplate = ` -// Code generated by kratos tool genmc. DO NOT EDIT. - -NEWLINE -/* - Package {{.PkgName}} is a generated mc cache package. - It is generated from: - ARGS -*/ -NEWLINE - -package {{.PkgName}} - -import ( - "context" - "fmt" - {{if .UseStrConv}}"strconv"{{end}} - {{if .EnableBatch }}"sync"{{end}} -NEWLINE - {{if .UseMemcached }}"github.com/go-kratos/kratos/pkg/cache/memcache"{{end}} - {{if .EnableBatch }}"github.com/go-kratos/kratos/pkg/sync/errgroup"{{end}} - "github.com/go-kratos/kratos/pkg/log" - {{.ImportPackage}} -) - -var _ _mc -` diff --git a/tool/kratos-gen-mc/main.go b/tool/kratos-gen-mc/main.go deleted file mode 100644 index 8a4f30002..000000000 --- a/tool/kratos-gen-mc/main.go +++ /dev/null @@ -1,570 +0,0 @@ -package main - -import ( - "bytes" - "flag" - "go/ast" - "io/ioutil" - "log" - "os" - "path/filepath" - "regexp" - "runtime" - "strconv" - "strings" - "text/template" - - common "github.com/go-kratos/kratos/tool/pkg" -) - -var ( - encode = flag.String("encode", "", "encode type: json/pb/raw/gob/gzip") - mcType = flag.String("type", "", "type: get/set/del/replace/only_add") - key = flag.String("key", "", "key name method") - expire = flag.String("expire", "", "expire time code") - structName = flag.String("struct_name", "dao", "struct name") - batchSize = flag.Int("batch", 0, "batch size") - batchErr = flag.String("batch_err", "break", "batch err to continue or break") - maxGroup = flag.Int("max_group", 0, "max group size") - checkNullCode = flag.String("check_null_code", "", "check null code") - nullExpire = flag.String("null_expire", "", "null cache expire time code") - - mcValidTypes = []string{"set", "replace", "del", "get", "only_add"} - mcValidPrefix = []string{"set", "replace", "del", "get", "cache", "add"} - optionNamesMap = map[string]bool{"batch": true, "max_group": true, "encode": true, "type": true, "key": true, "expire": true, "batch_err": true, "struct_name": true, "check_null_code": true, "null_expire": true} - simpleTypes = []string{"int", "int8", "int16", "int32", "int64", "float32", "float64", "uint", "uint8", "uint16", "uint32", "uint64", "bool", "string", "[]byte"} - lenTypes = []string{"[]", "map"} -) - -const ( - _interfaceName = "_mc" - _multiTpl = 1 - _singleTpl = 2 - _noneTpl = 3 - _typeGet = "get" - _typeSet = "set" - _typeDel = "del" - _typeReplace = "replace" - _typeAdd = "only_add" -) - -func resetFlag() { - *encode = "" - *mcType = "" - *batchSize = 0 - *maxGroup = 0 - *batchErr = "break" - *checkNullCode = "" - *nullExpire = "" - *structName = "dao" -} - -// options options -type options struct { - name string - keyType string - ValueType string - template int - SimpleValue bool - // int float 类型 - GetSimpleValue bool - // string, []byte类型 - GetDirectValue bool - ConvertValue2Bytes string - ConvertBytes2Value string - GoValue bool - ImportPackage string - importPackages []string - Args string - PkgName string - ExtraArgsType string - ExtraArgs string - MCType string - KeyMethod string - ExpireCode string - Encode string - UseMemcached bool - OriginValueType string - UseStrConv bool - Comment string - GroupSize int - MaxGroup int - EnableBatch bool - BatchErrBreak bool - LenType bool - PointType bool - StructName string - CheckNullCode string - ExpireNullCode string - EnableNullCode bool -} - -func getOptions(opt *options, comment string) { - os.Args = []string{os.Args[0]} - if regexp.MustCompile(`\s+//\s*mc:.+`).Match([]byte(comment)) { - args := strings.Split(common.RegexpReplace(`//\s*mc:(?P.+)`, comment, "$arg"), " ") - for _, arg := range args { - arg = strings.TrimSpace(arg) - if arg != "" { - // validate option name - argName := common.RegexpReplace(`-(?P[\w_-]+)=.+`, arg, "$name") - if !optionNamesMap[argName] { - log.Fatalf("选项:%s 不存在 请检查拼写\n", argName) - } - os.Args = append(os.Args, arg) - } - } - } - resetFlag() - flag.Parse() - if *mcType != "" { - opt.MCType = *mcType - } - if *key != "" { - opt.KeyMethod = *key - } - if *expire != "" { - opt.ExpireCode = *expire - } - opt.EnableBatch = (*batchSize != 0) && (*maxGroup != 0) - opt.BatchErrBreak = *batchErr == "break" - opt.GroupSize = *batchSize - opt.MaxGroup = *maxGroup - opt.StructName = *structName - opt.CheckNullCode = *checkNullCode - if *nullExpire != "" { - opt.ExpireNullCode = *nullExpire - } - if opt.CheckNullCode != "" { - opt.EnableNullCode = true - } -} - -func getTypeFromPrefix(opt *options, params []*ast.Field, s *common.Source) { - if opt.MCType == "" { - for _, t := range mcValidPrefix { - if strings.HasPrefix(strings.ToLower(opt.name), t) { - if t == "add" { - t = _typeSet - } - opt.MCType = t - break - } - } - if opt.MCType == "" { - log.Fatalln(opt.name + "请指定方法类型(type=get/set/del...)") - } - } - if opt.MCType == "cache" { - opt.MCType = _typeGet - } - if len(params) == 0 { - log.Fatalln(opt.name + "参数不足") - } - for _, p := range params { - if len(p.Names) > 1 { - log.Fatalln(opt.name + "不支持省略类型 请写全声明中的字段类型名称") - } - } - if s.ExprString(params[0].Type) != "context.Context" { - log.Fatalln(opt.name + "第一个参数必须为context") - } - for _, param := range params { - if len(param.Names) > 1 { - log.Fatalln(opt.name + "不支持省略类型") - } - } -} - -func processList(s *common.Source, list *ast.Field) (opt options) { - src := s.Src - fset := s.Fset - lines := strings.Split(src, "\n") - opt = options{Args: s.GetDef(_interfaceName), UseMemcached: true, importPackages: s.Packages(list)} - opt.name = list.Names[0].Name - opt.KeyMethod = "key" + opt.name - opt.ExpireCode = "d.mc" + opt.name + "Expire" - opt.ExpireNullCode = "300" // 默认5分钟 - // get comment - line := fset.Position(list.Pos()).Line - 3 - if len(lines)-1 >= line { - comment := lines[line] - opt.Comment = common.RegexpReplace(`\s+//(?P.+)`, comment, "$name") - opt.Comment = strings.TrimSpace(opt.Comment) - } - // get options - line = fset.Position(list.Pos()).Line - 2 - comment := lines[line] - getOptions(&opt, comment) - // get type from prefix - params := list.Type.(*ast.FuncType).Params.List - getTypeFromPrefix(&opt, params, s) - // get template - if len(params) == 1 { - opt.template = _noneTpl - } else if (len(params) == 2) && (opt.MCType == _typeSet || opt.MCType == _typeAdd || opt.MCType == _typeReplace) { - if _, ok := params[1].Type.(*ast.MapType); ok { - opt.template = _multiTpl - } else { - opt.template = _noneTpl - } - } else { - if _, ok := params[1].Type.(*ast.ArrayType); ok { - opt.template = _multiTpl - } else if _, ok := params[1].Type.(*ast.MapType); ok { - opt.template = _multiTpl - } else { - opt.template = _singleTpl - } - } - // extra args - if len(params) > 2 { - args := []string{""} - allArgs := []string{""} - var pos = 2 - if (opt.MCType == _typeAdd) || (opt.MCType == _typeSet) || (opt.MCType == _typeReplace) { - pos = 3 - } - if opt.template == _multiTpl && opt.MCType == _typeSet { - pos = 2 - } - for _, pa := range params[pos:] { - paType := s.ExprString(pa.Type) - if len(pa.Names) == 0 { - args = append(args, paType) - allArgs = append(allArgs, paType) - continue - } - var names []string - for _, name := range pa.Names { - names = append(names, name.Name) - } - allArgs = append(allArgs, strings.Join(names, ",")+" "+paType) - args = append(args, strings.Join(names, ",")) - } - if len(args) > 1 { - opt.ExtraArgs = strings.Join(args, ",") - opt.ExtraArgsType = strings.Join(allArgs, ",") - } - } - results := list.Type.(*ast.FuncType).Results.List - getKeyValueType(&opt, params, results, s) - return -} - -func getKeyValueType(opt *options, params, results []*ast.Field, s *common.Source) { - // check - if s.ExprString(results[len(results)-1].Type) != "error" { - log.Fatalln("最后返回值参数需为error") - } - for _, res := range results { - if len(res.Names) > 1 { - log.Fatalln(opt.name + "返回值不支持省略类型") - } - } - if opt.MCType == _typeGet { - if len(results) != 2 { - log.Fatalln("参数个数不对") - } - } - // get key type and value type - if (opt.MCType == _typeAdd) || (opt.MCType == _typeSet) || (opt.MCType == _typeReplace) { - if opt.template == _multiTpl { - p, ok := params[1].Type.(*ast.MapType) - if !ok { - log.Fatalf("%s: 参数类型错误 批量设置数据时类型需为map类型\n", opt.name) - } - opt.keyType = s.ExprString(p.Key) - opt.ValueType = s.ExprString(p.Value) - } else if opt.template == _singleTpl { - opt.keyType = s.ExprString(params[1].Type) - opt.ValueType = s.ExprString(params[2].Type) - } else { - opt.ValueType = s.ExprString(params[1].Type) - } - } - if opt.MCType == _typeGet { - if opt.template == _multiTpl { - if p, ok := results[0].Type.(*ast.MapType); ok { - opt.keyType = s.ExprString(p.Key) - opt.ValueType = s.ExprString(p.Value) - } else { - log.Fatalf("%s: 返回值类型错误 批量获取数据时返回值需为map类型\n", opt.name) - } - } else if opt.template == _singleTpl { - opt.keyType = s.ExprString(params[1].Type) - opt.ValueType = s.ExprString(results[0].Type) - } else { - opt.ValueType = s.ExprString(results[0].Type) - } - } - if opt.MCType == _typeDel { - if opt.template == _multiTpl { - p, ok := params[1].Type.(*ast.ArrayType) - if !ok { - log.Fatalf("%s: 类型错误 参数需为[]类型\n", opt.name) - } - opt.keyType = s.ExprString(p.Elt) - } else if opt.template == _singleTpl { - opt.keyType = s.ExprString(params[1].Type) - } - } - for _, t := range simpleTypes { - if t == opt.ValueType { - opt.SimpleValue = true - opt.GetSimpleValue = true - opt.ConvertValue2Bytes = convertValue2Bytes(t) - opt.ConvertBytes2Value = convertBytes2Value(t) - break - } - } - if opt.ValueType == "string" { - opt.LenType = true - } else { - for _, t := range lenTypes { - if strings.HasPrefix(opt.ValueType, t) { - opt.LenType = true - break - } - } - } - if opt.SimpleValue && (opt.ValueType == "[]byte" || opt.ValueType == "string") { - opt.GetSimpleValue = false - opt.GetDirectValue = true - } - if opt.MCType == _typeGet && opt.template == _multiTpl { - opt.UseMemcached = false - } - if strings.HasPrefix(opt.ValueType, "*") { - opt.PointType = true - opt.OriginValueType = strings.Replace(opt.ValueType, "*", "", 1) - } else { - opt.OriginValueType = opt.ValueType - } - if *encode != "" { - var flags []string - for _, f := range strings.Split(*encode, "|") { - switch f { - case "gob": - flags = append(flags, "memcache.FlagGOB") - case "json": - flags = append(flags, "memcache.FlagJSON") - case "raw": - flags = append(flags, "memcache.FlagRAW") - case "pb": - flags = append(flags, "memcache.FlagProtobuf") - case "gzip": - flags = append(flags, "memcache.FlagGzip") - default: - log.Fatalf("%s: encode类型无效\n", opt.name) - } - } - opt.Encode = strings.Join(flags, " | ") - } else { - if opt.SimpleValue { - opt.Encode = "memcache.FlagRAW" - } else { - opt.Encode = "memcache.FlagJSON" - } - } -} - -func parse(s *common.Source) (opts []*options) { - c := s.F.Scope.Lookup(_interfaceName) - if (c == nil) || (c.Kind != ast.Typ) { - log.Fatalln("无法找到缓存声明") - } - lists := c.Decl.(*ast.TypeSpec).Type.(*ast.InterfaceType).Methods.List - for _, list := range lists { - opt := processList(s, list) - opt.Check() - opts = append(opts, &opt) - } - return -} - -func (option *options) Check() { - var valid bool - for _, x := range mcValidTypes { - if x == option.MCType { - valid = true - break - } - } - if !valid { - log.Fatalf("%s: 类型错误 不支持%s类型\n", option.name, option.MCType) - } - if (option.MCType != _typeDel) && !option.SimpleValue && !strings.Contains(option.ValueType, "*") && !strings.Contains(option.ValueType, "[]") && !strings.Contains(option.ValueType, "map") { - log.Fatalf("%s: 值类型只能为基本类型/slice/map/指针类型\n", option.name) - } -} - -func genHeader(opts []*options) (src string) { - option := options{PkgName: os.Getenv("GOPACKAGE"), UseMemcached: false} - var packages []string - packagesMap := map[string]bool{`"context"`: true} - for _, opt := range opts { - if len(opt.importPackages) > 0 { - for _, pkg := range opt.importPackages { - if !packagesMap[pkg] { - packages = append(packages, pkg) - packagesMap[pkg] = true - } - } - } - if opt.Args != "" { - option.Args = opt.Args - } - if opt.UseMemcached { - option.UseMemcached = true - } - if opt.SimpleValue && !opt.GetDirectValue { - option.UseStrConv = true - } - if opt.EnableBatch { - option.EnableBatch = true - } - } - option.ImportPackage = strings.Join(packages, "\n") - src = _headerTemplate - t := template.Must(template.New("header").Parse(src)) - var buffer bytes.Buffer - err := t.Execute(&buffer, option) - if err != nil { - log.Fatalf("execute template: %s", err) - } - // Format the output. - src = strings.Replace(buffer.String(), "\t", "", -1) - src = regexp.MustCompile("\n+").ReplaceAllString(src, "\n") - src = strings.Replace(src, "NEWLINE", "", -1) - src = strings.Replace(src, "ARGS", option.Args, -1) - return -} - -func getNewTemplate(option *options) (src string) { - if option.template == _multiTpl { - switch option.MCType { - case _typeGet: - src = _multiGetTemplate - case _typeSet: - src = _multiSetTemplate - case _typeReplace: - src = _multiReplaceTemplate - case _typeDel: - src = _multiDelTemplate - case _typeAdd: - src = _multiAddTemplate - } - } else if option.template == _singleTpl { - switch option.MCType { - case _typeGet: - src = _singleGetTemplate - case _typeSet: - src = _singleSetTemplate - case _typeReplace: - src = _singleReplaceTemplate - case _typeDel: - src = _singleDelTemplate - case _typeAdd: - src = _singleAddTemplate - } - } else { - switch option.MCType { - case _typeGet: - src = _noneGetTemplate - case _typeSet: - src = _noneSetTemplate - case _typeReplace: - src = _noneReplaceTemplate - case _typeDel: - src = _noneDelTemplate - case _typeAdd: - src = _noneAddTemplate - } - } - return -} - -func genBody(opts []*options) (res string) { - for _, option := range opts { - src := getNewTemplate(option) - src = strings.Replace(src, "KEY", option.keyType, -1) - src = strings.Replace(src, "NAME", option.name, -1) - src = strings.Replace(src, "VALUE", option.ValueType, -1) - src = strings.Replace(src, "GROUPSIZE", strconv.Itoa(option.GroupSize), -1) - src = strings.Replace(src, "MAXGROUP", strconv.Itoa(option.MaxGroup), -1) - if option.EnableNullCode { - option.CheckNullCode = strings.Replace(option.CheckNullCode, "$", "val", -1) - } - t := template.Must(template.New("cache").Parse(src)) - var buffer bytes.Buffer - err := t.Execute(&buffer, option) - if err != nil { - log.Fatalf("execute template: %s", err) - } - // Format the output. - src = strings.Replace(buffer.String(), "\t", "", -1) - src = regexp.MustCompile("\n+").ReplaceAllString(src, "\n") - res = res + "\n" + src - } - return -} - -func main() { - log.SetFlags(0) - defer func() { - if err := recover(); err != nil { - buf := make([]byte, 64*1024) - buf = buf[:runtime.Stack(buf, false)] - log.Fatalf("程序解析失败, err: %+v stack: %s", err, buf) - } - }() - options := parse(common.NewSource(common.SourceText())) - header := genHeader(options) - body := genBody(options) - code := common.FormatCode(header + "\n" + body) - // Write to file. - dir := filepath.Dir(".") - outputName := filepath.Join(dir, "mc.cache.go") - err := ioutil.WriteFile(outputName, []byte(code), 0644) - if err != nil { - log.Fatalf("写入文件失败: %s", err) - } - log.Println("mc.cache.go: 生成成功") -} - -func convertValue2Bytes(t string) string { - switch t { - case "int", "int8", "int16", "int32", "int64": - return "[]byte(strconv.FormatInt(int64(val), 10))" - case "uint", "uint8", "uint16", "uint32", "uint64": - return "[]byte(strconv.FormatUInt(val, 10))" - case "bool": - return "[]byte(strconv.FormatBool(val))" - case "float32": - return "[]byte(strconv.FormatFloat(val, 'E', -1, 32))" - case "float64": - return "[]byte(strconv.FormatFloat(val, 'E', -1, 64))" - case "string": - return "[]byte(val)" - case "[]byte": - return "val" - } - return "" -} - -func convertBytes2Value(t string) string { - switch t { - case "int", "int8", "int16", "int32", "int64": - return "strconv.ParseInt(v, 10, 64)" - case "uint", "uint8", "uint16", "uint32", "uint64": - return "strconv.ParseUInt(v, 10, 64)" - case "bool": - return "strconv.ParseBool(v)" - case "float32": - return "float32(strconv.ParseFloat(v, 32))" - case "float64": - return "strconv.ParseFloat(v, 64)" - } - return "" -} diff --git a/tool/kratos-gen-mc/multi_template.go b/tool/kratos-gen-mc/multi_template.go deleted file mode 100644 index db02358cc..000000000 --- a/tool/kratos-gen-mc/multi_template.go +++ /dev/null @@ -1,205 +0,0 @@ -package main - -import ( - "strings" -) - -var _multiGetTemplate = ` -// NAME {{or .Comment "get data from mc"}} -func (d *{{.StructName}}) NAME(c context.Context, ids []KEY {{.ExtraArgsType}}) (res map[KEY]VALUE, err error) { - l := len(ids) - if l == 0 { - return - } - {{if .EnableBatch}} - mutex := sync.Mutex{} - for i:=0;i < l; i+= GROUPSIZE * MAXGROUP { - var subKeys []KEY - {{if .BatchErrBreak}} - group, ctx := errgroup.WithContext(c) - {{else}} - group := &errgroup.Group{} - ctx := c - {{end}} - if (i + GROUPSIZE * MAXGROUP) > l { - subKeys = ids[i:] - } else { - subKeys = ids[i : i+GROUPSIZE * MAXGROUP] - } - subLen := len(subKeys) - for j:=0; j< subLen; j += GROUPSIZE { - var ks []KEY - if (j+GROUPSIZE) > subLen { - ks = subKeys[j:] - } else { - ks = subKeys[j:j+GROUPSIZE] - } - group.Go(func() (err error) { - keysMap := make(map[string]KEY, len(ks)) - keys := make([]string, 0, len(ks)) - for _, id := range ks { - key := {{.KeyMethod}}(id{{.ExtraArgs}}) - keysMap[key] = id - keys = append(keys, key) - } - replies, err := d.mc.GetMulti(c, keys) - if err != nil { - log.Errorv(ctx, log.KV("NAME", fmt.Sprintf("%+v", err)), log.KV("keys", keys)) - return - } - for _, key := range replies.Keys() { - {{if .GetSimpleValue}} - var v string - err = replies.Scan(key, &v) - {{else}} - var v VALUE - {{if .GetDirectValue}} - err = replies.Scan(key, &v) - {{else}} - {{if .PointType}} - v = &{{.OriginValueType}}{} - err = replies.Scan(key, v) - {{else}} - v = {{.OriginValueType}}{} - err = replies.Scan(key, &v) - {{end}} - {{end}} - {{end}} - if err != nil { - log.Errorv(ctx, log.KV("NAME", fmt.Sprintf("%+v", err)), log.KV("key", key)) - return - } - {{if .GetSimpleValue}} - r, err := {{.ConvertBytes2Value}} - if err != nil { - log.Errorv(ctx, log.KV("NAME", fmt.Sprintf("%+v", err)), log.KV("key", key)) - return res, err - } - mutex.Lock() - if res == nil { - res = make(map[KEY]VALUE, len(keys)) - } - res[keysMap[key]] = {{.ValueType}}(r) - mutex.Unlock() - {{else}} - mutex.Lock() - if res == nil { - res = make(map[KEY]VALUE, len(keys)) - } - res[keysMap[key]] = v - mutex.Unlock() - {{end}} - } - return - }) - } - err1 := group.Wait() - if err1 != nil { - err = err1 - {{if .BatchErrBreak}} - break - {{end}} - } - } - {{else}} - keysMap := make(map[string]KEY, l) - keys := make([]string, 0, l) - for _, id := range ids { - key := {{.KeyMethod}}(id{{.ExtraArgs}}) - keysMap[key] = id - keys = append(keys, key) - } - replies, err := d.mc.GetMulti(c, keys) - if err != nil { - log.Errorv(c, log.KV("NAME", fmt.Sprintf("%+v", err)), log.KV("keys", keys)) - return - } - for _, key := range replies.Keys() { - {{if .GetSimpleValue}} - var v string - err = replies.Scan(key, &v) - {{else}} - {{if .PointType}} - v := &{{.OriginValueType}}{} - err = replies.Scan(key, v) - {{else}} - v := {{.OriginValueType}}{} - err = replies.Scan(key, &v) - {{end}} - {{end}} - if err != nil { - log.Errorv(c, log.KV("NAME", fmt.Sprintf("%+v", err)), log.KV("key", key)) - return - } - {{if .GetSimpleValue}} - r, err := {{.ConvertBytes2Value}} - if err != nil { - log.Errorv(c, log.KV("NAME", fmt.Sprintf("%+v", err)), log.KV("key", key)) - return res, err - } - if res == nil { - res = make(map[KEY]VALUE, len(keys)) - } - res[keysMap[key]] = {{.ValueType}}(r) - {{else}} - if res == nil { - res = make(map[KEY]VALUE, len(keys)) - } - res[keysMap[key]] = v - {{end}} - } - {{end}} - return -} -` - -var _multiSetTemplate = ` -// NAME {{or .Comment "Set data to mc"}} -func (d *{{.StructName}}) NAME(c context.Context, values map[KEY]VALUE {{.ExtraArgsType}}) (err error) { - if len(values) == 0 { - return - } - for id, val := range values { - key := {{.KeyMethod}}(id{{.ExtraArgs}}) - {{if .SimpleValue}} - bs := {{.ConvertValue2Bytes}} - item := &memcache.Item{Key: key, Value: bs, Expiration: {{.ExpireCode}}, Flags: {{.Encode}}} - {{else}} - item := &memcache.Item{Key: key, Object: val, Expiration: {{.ExpireCode}}, Flags: {{.Encode}}} - {{end}} - {{if .EnableNullCode}} - if {{.CheckNullCode}} { - item.Expiration = {{.ExpireNullCode}} - } - {{end}} - if err = d.mc.Set(c, item); err != nil { - log.Errorv(c, log.KV("NAME", fmt.Sprintf("%+v", err)), log.KV("key", key)) - return - } - } - return -} -` -var _multiAddTemplate = strings.Replace(_multiSetTemplate, "Set", "Add", -1) -var _multiReplaceTemplate = strings.Replace(_multiSetTemplate, "Set", "Replace", -1) - -var _multiDelTemplate = ` -// NAME {{or .Comment "delete data from mc"}} -func (d *{{.StructName}}) NAME(c context.Context, ids []KEY {{.ExtraArgsType}}) (err error) { - if len(ids) == 0 { - return - } - for _, id := range ids { - key := {{.KeyMethod}}(id{{.ExtraArgs}}) - if err = d.mc.Delete(c, key); err != nil { - if err == memcache.ErrNotFound { - err = nil - continue - } - log.Errorv(c, log.KV("NAME", fmt.Sprintf("%+v", err)), log.KV("key", key)) - return - } - } - return -} -` diff --git a/tool/kratos-gen-mc/none_template.go b/tool/kratos-gen-mc/none_template.go deleted file mode 100644 index 65ec1de63..000000000 --- a/tool/kratos-gen-mc/none_template.go +++ /dev/null @@ -1,104 +0,0 @@ -package main - -import ( - "strings" -) - -var _noneGetTemplate = ` -// NAME {{or .Comment "get data from mc"}} -func (d *{{.StructName}}) NAME(c context.Context) (res VALUE, err error) { - key := {{.KeyMethod}}() - {{if .GetSimpleValue}} - var v string - err = d.mc.Get(c, key).Scan(&v) - {{else}} - {{if .GetDirectValue}} - err = d.mc.Get(c, key).Scan(&res) - {{else}} - {{if .PointType}} - res = &{{.OriginValueType}}{} - if err = d.mc.Get(c, key).Scan(res); err != nil { - res = nil - if err == memcache.ErrNotFound { - err = nil - return - } - } - {{else}} - err = d.mc.Get(c, key).Scan(&res) - {{end}} - {{end}} - {{end}} - if err != nil { - {{if .PointType}} - {{else}} - if err == memcache.ErrNotFound { - err = nil - return - } - {{end}} - log.Errorv(c, log.KV("NAME", fmt.Sprintf("%+v", err)), log.KV("key", key)) - return - } - {{if .GetSimpleValue}} - r, err := {{.ConvertBytes2Value}} - if err != nil { - log.Errorv(c, log.KV("NAME", fmt.Sprintf("%+v", err)), log.KV("key", key)) - return - } - res = {{.ValueType}}(r) - {{end}} - return -} -` - -var _noneSetTemplate = ` -// NAME {{or .Comment "Set data to mc"}} -func (d *{{.StructName}}) NAME(c context.Context, val VALUE) (err error) { - {{if .PointType}} - if val == nil { - return - } - {{end}} - {{if .LenType}} - if len(val) == 0 { - return - } - {{end}} - key := {{.KeyMethod}}() - {{if .SimpleValue}} - bs := {{.ConvertValue2Bytes}} - item := &memcache.Item{Key: key, Value: bs, Expiration: {{.ExpireCode}}, Flags: {{.Encode}}} - {{else}} - item := &memcache.Item{Key: key, Object: val, Expiration: {{.ExpireCode}}, Flags: {{.Encode}}} - {{end}} - {{if .EnableNullCode}} - if {{.CheckNullCode}} { - item.Expiration = {{.ExpireNullCode}} - } - {{end}} - if err = d.mc.Set(c, item); err != nil { - log.Errorv(c, log.KV("NAME", fmt.Sprintf("%+v", err)), log.KV("key", key)) - return - } - return -} -` -var _noneAddTemplate = strings.Replace(_noneSetTemplate, "Set", "Add", -1) -var _noneReplaceTemplate = strings.Replace(_noneSetTemplate, "Set", "Replace", -1) - -var _noneDelTemplate = ` -// NAME {{or .Comment "delete data from mc"}} -func (d *{{.StructName}}) NAME(c context.Context) (err error) { - key := {{.KeyMethod}}() - if err = d.mc.Delete(c, key); err != nil { - if err == memcache.ErrNotFound { - err = nil - return - } - log.Errorv(c, log.KV("NAME", fmt.Sprintf("%+v", err)), log.KV("key", key)) - return - } - return -} -` diff --git a/tool/kratos-gen-mc/single_template.go b/tool/kratos-gen-mc/single_template.go deleted file mode 100644 index 850a4ba12..000000000 --- a/tool/kratos-gen-mc/single_template.go +++ /dev/null @@ -1,103 +0,0 @@ -package main - -import ( - "strings" -) - -var _singleGetTemplate = ` -// NAME {{or .Comment "get data from mc"}} -func (d *{{.StructName}}) NAME(c context.Context, id KEY {{.ExtraArgsType}}) (res VALUE, err error) { - key := {{.KeyMethod}}(id{{.ExtraArgs}}) - {{if .GetSimpleValue}} - var v string - err = d.mc.Get(c, key).Scan(&v) - {{else}} - {{if .GetDirectValue}} - err = d.mc.Get(c, key).Scan(&res) - {{else}} - {{if .PointType}} - res = &{{.OriginValueType}}{} - if err = d.mc.Get(c, key).Scan(res); err != nil { - res = nil - if err == memcache.ErrNotFound { - err = nil - } - } - {{else}} - err = d.mc.Get(c, key).Scan(&res) - {{end}} - {{end}} - {{end}} - if err != nil { - {{if .PointType}} - {{else}} - if err == memcache.ErrNotFound { - err = nil - return - } - {{end}} - log.Errorv(c, log.KV("NAME", fmt.Sprintf("%+v", err)), log.KV("key", key)) - return - } - {{if .GetSimpleValue}} - r, err := {{.ConvertBytes2Value}} - if err != nil { - log.Errorv(c, log.KV("NAME", fmt.Sprintf("%+v", err)), log.KV("key", key)) - return - } - res = {{.ValueType}}(r) - {{end}} - return -} -` - -var _singleSetTemplate = ` -// NAME {{or .Comment "Set data to mc"}} -func (d *{{.StructName}}) NAME(c context.Context, id KEY, val VALUE {{.ExtraArgsType}}) (err error) { - {{if .PointType}} - if val == nil { - return - } - {{end}} - {{if .LenType}} - if len(val) == 0 { - return - } - {{end}} - key := {{.KeyMethod}}(id{{.ExtraArgs}}) - {{if .SimpleValue}} - bs := {{.ConvertValue2Bytes}} - item := &memcache.Item{Key: key, Value: bs, Expiration: {{.ExpireCode}}, Flags: {{.Encode}}} - {{else}} - item := &memcache.Item{Key: key, Object: val, Expiration: {{.ExpireCode}}, Flags: {{.Encode}}} - {{end}} - {{if .EnableNullCode}} - if {{.CheckNullCode}} { - item.Expiration = {{.ExpireNullCode}} - } - {{end}} - if err = d.mc.Set(c, item); err != nil { - log.Errorv(c, log.KV("NAME", fmt.Sprintf("%+v", err)), log.KV("key", key)) - return - } - return -} -` -var _singleAddTemplate = strings.Replace(_singleSetTemplate, "Set", "Add", -1) -var _singleReplaceTemplate = strings.Replace(_singleSetTemplate, "Set", "Replace", -1) - -var _singleDelTemplate = ` -// NAME {{or .Comment "delete data from mc"}} -func (d *{{.StructName}}) NAME(c context.Context, id KEY {{.ExtraArgsType}}) (err error) { - key := {{.KeyMethod}}(id{{.ExtraArgs}}) - if err = d.mc.Delete(c, key); err != nil { - if err == memcache.ErrNotFound { - err = nil - return - } - log.Errorv(c, log.KV("NAME", fmt.Sprintf("%+v", err)), log.KV("key", key)) - return - } - return -} -` diff --git a/tool/kratos-gen-mc/testdata/dao.go b/tool/kratos-gen-mc/testdata/dao.go deleted file mode 100644 index a9a0e9d8e..000000000 --- a/tool/kratos-gen-mc/testdata/dao.go +++ /dev/null @@ -1,93 +0,0 @@ -package testdata - -import ( - "context" - "fmt" - "time" - - "github.com/go-kratos/kratos/pkg/cache/memcache" - "github.com/go-kratos/kratos/pkg/container/pool" - xtime "github.com/go-kratos/kratos/pkg/time" -) - -// Dao . -type Dao struct { - mc *memcache.Memcache - demoExpire int32 -} - -// New new dao -func New() (d *Dao) { - cfg := &memcache.Config{ - Config: &pool.Config{ - Active: 10, - Idle: 5, - IdleTimeout: xtime.Duration(time.Second), - }, - Name: "test", - Proto: "tcp", - // Addr: "172.16.33.54:11214", - Addr: "127.0.0.1:11211", - DialTimeout: xtime.Duration(time.Second), - ReadTimeout: xtime.Duration(time.Second), - WriteTimeout: xtime.Duration(time.Second), - } - d = &Dao{ - mc: memcache.New(cfg), - demoExpire: int32(5), - } - return -} - -//go:generate kratos tool genmc -type _mc interface { - // mc: -key=demoKey - CacheDemos(c context.Context, keys []int64) (map[int64]*Demo, error) - // mc: -key=demoKey - CacheDemo(c context.Context, key int64) (*Demo, error) - // mc: -key=keyMid - CacheDemo1(c context.Context, key int64, mid int64) (*Demo, error) - // mc: -key=noneKey - CacheNone(c context.Context) (*Demo, error) - // mc: -key=demoKey - CacheString(c context.Context, key int64) (string, error) - - // mc: -key=demoKey -expire=d.demoExpire -encode=json - AddCacheDemos(c context.Context, values map[int64]*Demo) error - // mc: -key=demo2Key -expire=d.demoExpire -encode=json - AddCacheDemos2(c context.Context, values map[int64]*Demo, tp int64) error - // 这里也支持自定义注释 会替换默认的注释 - // mc: -key=demoKey -expire=d.demoExpire -encode=json|gzip - AddCacheDemo(c context.Context, key int64, value *Demo) error - // mc: -key=keyMid -expire=d.demoExpire -encode=gob - AddCacheDemo1(c context.Context, key int64, value *Demo, mid int64) error - // mc: -key=noneKey - AddCacheNone(c context.Context, value *Demo) error - // mc: -key=demoKey -expire=d.demoExpire - AddCacheString(c context.Context, key int64, value string) error - - // mc: -key=demoKey - DelCacheDemos(c context.Context, keys []int64) error - // mc: -key=demoKey - DelCacheDemo(c context.Context, key int64) error - // mc: -key=keyMid - DelCacheDemo1(c context.Context, key int64, mid int64) error - // mc: -key=noneKey - DelCacheNone(c context.Context) error -} - -func demoKey(id int64) string { - return fmt.Sprintf("art_%d", id) -} - -func demo2Key(id, tp int64) string { - return fmt.Sprintf("art_%d_%d", id, tp) -} - -func keyMid(id, mid int64) string { - return fmt.Sprintf("art_%d_%d", id, mid) -} - -func noneKey() string { - return "none" -} diff --git a/tool/kratos-gen-mc/testdata/dao_test.go b/tool/kratos-gen-mc/testdata/dao_test.go deleted file mode 100644 index 0bf8a0d59..000000000 --- a/tool/kratos-gen-mc/testdata/dao_test.go +++ /dev/null @@ -1,116 +0,0 @@ -package testdata - -import ( - "context" - "testing" -) - -func TestDemo(t *testing.T) { - d := New() - c := context.TODO() - art := &Demo{ID: 1, Title: "title"} - err := d.AddCacheDemo(c, art.ID, art) - if err != nil { - t.Errorf("err should be nil, get: %v", err) - t.FailNow() - } - art1, err := d.CacheDemo(c, art.ID) - if err != nil { - t.Errorf("err should be nil, get: %v", err) - t.FailNow() - } - if (art1.ID != art.ID) || (art.Title != art1.Title) { - t.Error("art not equal") - t.FailNow() - } - err = d.DelCacheDemo(c, art.ID) - if err != nil { - t.Errorf("err should be nil, get: %v", err) - t.FailNow() - } - art1, err = d.CacheDemo(c, art.ID) - if (art1 != nil) || (err != nil) { - t.Errorf("art %v, err: %v", art1, err) - t.FailNow() - } -} - -func TestNone(t *testing.T) { - d := New() - c := context.TODO() - art := &Demo{ID: 1, Title: "title"} - err := d.AddCacheNone(c, art) - if err != nil { - t.Errorf("err should be nil, get: %v", err) - t.FailNow() - } - art1, err := d.CacheNone(c) - if err != nil { - t.Errorf("err should be nil, get: %v", err) - t.FailNow() - } - if (art1.ID != art.ID) || (art.Title != art1.Title) { - t.Error("art not equal") - t.FailNow() - } - err = d.DelCacheNone(c) - if err != nil { - t.Errorf("err should be nil, get: %v", err) - t.FailNow() - } - art1, err = d.CacheNone(c) - if (art1 != nil) || (err != nil) { - t.Errorf("art %v, err: %v", art1, err) - t.FailNow() - } -} - -func TestDemos(t *testing.T) { - d := New() - c := context.TODO() - art1 := &Demo{ID: 1, Title: "title"} - art2 := &Demo{ID: 2, Title: "title"} - err := d.AddCacheDemos(c, map[int64]*Demo{1: art1, 2: art2}) - if err != nil { - t.Errorf("err should be nil, get: %v", err) - t.FailNow() - } - arts, err := d.CacheDemos(c, []int64{art1.ID, art2.ID}) - if err != nil { - t.Errorf("err should be nil, get: %v", err) - t.FailNow() - } - if (arts[1].Title != art1.Title) || (arts[2].Title != art2.Title) { - t.Error("art not equal") - t.FailNow() - } - err = d.DelCacheDemos(c, []int64{art1.ID, art2.ID}) - if err != nil { - t.Errorf("err should be nil, get: %v", err) - t.FailNow() - } - arts, err = d.CacheDemos(c, []int64{art1.ID, art2.ID}) - if (arts != nil) || (err != nil) { - t.Errorf("art %v, err: %v", art1, err) - t.FailNow() - } -} - -func TestString(t *testing.T) { - d := New() - c := context.TODO() - err := d.AddCacheString(c, 1, "abc") - if err != nil { - t.Errorf("err should be nil, get: %v", err) - t.FailNow() - } - res, err := d.CacheString(c, 1) - if err != nil { - t.Errorf("err should be nil, get: %v", err) - t.FailNow() - } - if res != "abc" { - t.Error("res wrong") - t.FailNow() - } -} diff --git a/tool/kratos-gen-mc/testdata/mc.cache.go b/tool/kratos-gen-mc/testdata/mc.cache.go deleted file mode 100644 index bfde3c273..000000000 --- a/tool/kratos-gen-mc/testdata/mc.cache.go +++ /dev/null @@ -1,305 +0,0 @@ -// Code generated by kratos tool genmc. DO NOT EDIT. - -/* - Package testdata is a generated mc cache package. - It is generated from: - type _mc interface { - // mc: -key=demoKey - CacheDemos(c context.Context, keys []int64) (map[int64]*Demo, error) - // mc: -key=demoKey - CacheDemo(c context.Context, key int64) (*Demo, error) - // mc: -key=keyMid - CacheDemo1(c context.Context, key int64, mid int64) (*Demo, error) - // mc: -key=noneKey - CacheNone(c context.Context) (*Demo, error) - // mc: -key=demoKey - CacheString(c context.Context, key int64) (string, error) - - // mc: -key=demoKey -expire=d.demoExpire -encode=json - AddCacheDemos(c context.Context, values map[int64]*Demo) error - // mc: -key=demo2Key -expire=d.demoExpire -encode=json - AddCacheDemos2(c context.Context, values map[int64]*Demo, tp int64) error - // 这里也支持自定义注释 会替换默认的注释 - // mc: -key=demoKey -expire=d.demoExpire -encode=json|gzip - AddCacheDemo(c context.Context, key int64, value *Demo) error - // mc: -key=keyMid -expire=d.demoExpire -encode=gob - AddCacheDemo1(c context.Context, key int64, value *Demo, mid int64) error - // mc: -key=noneKey - AddCacheNone(c context.Context, value *Demo) error - // mc: -key=demoKey -expire=d.demoExpire - AddCacheString(c context.Context, key int64, value string) error - - // mc: -key=demoKey - DelCacheDemos(c context.Context, keys []int64) error - // mc: -key=demoKey - DelCacheDemo(c context.Context, key int64) error - // mc: -key=keyMid - DelCacheDemo1(c context.Context, key int64, mid int64) error - // mc: -key=noneKey - DelCacheNone(c context.Context) error - } -*/ - -package testdata - -import ( - "context" - "fmt" - - "github.com/go-kratos/kratos/pkg/cache/memcache" - "github.com/go-kratos/kratos/pkg/log" -) - -var ( - _ _mc -) - -// CacheDemos get data from mc -func (d *Dao) CacheDemos(c context.Context, ids []int64) (res map[int64]*Demo, err error) { - l := len(ids) - if l == 0 { - return - } - keysMap := make(map[string]int64, l) - keys := make([]string, 0, l) - for _, id := range ids { - key := demoKey(id) - keysMap[key] = id - keys = append(keys, key) - } - replies, err := d.mc.GetMulti(c, keys) - if err != nil { - log.Errorv(c, log.KV("CacheDemos", fmt.Sprintf("%+v", err)), log.KV("keys", keys)) - return - } - for _, key := range replies.Keys() { - v := &Demo{} - err = replies.Scan(key, v) - if err != nil { - log.Errorv(c, log.KV("CacheDemos", fmt.Sprintf("%+v", err)), log.KV("key", key)) - return - } - if res == nil { - res = make(map[int64]*Demo, len(keys)) - } - res[keysMap[key]] = v - } - return -} - -// CacheDemo get data from mc -func (d *Dao) CacheDemo(c context.Context, id int64) (res *Demo, err error) { - key := demoKey(id) - res = &Demo{} - if err = d.mc.Get(c, key).Scan(res); err != nil { - res = nil - if err == memcache.ErrNotFound { - err = nil - } - } - if err != nil { - log.Errorv(c, log.KV("CacheDemo", fmt.Sprintf("%+v", err)), log.KV("key", key)) - return - } - return -} - -// CacheDemo1 get data from mc -func (d *Dao) CacheDemo1(c context.Context, id int64, mid int64) (res *Demo, err error) { - key := keyMid(id, mid) - res = &Demo{} - if err = d.mc.Get(c, key).Scan(res); err != nil { - res = nil - if err == memcache.ErrNotFound { - err = nil - } - } - if err != nil { - log.Errorv(c, log.KV("CacheDemo1", fmt.Sprintf("%+v", err)), log.KV("key", key)) - return - } - return -} - -// CacheNone get data from mc -func (d *Dao) CacheNone(c context.Context) (res *Demo, err error) { - key := noneKey() - res = &Demo{} - if err = d.mc.Get(c, key).Scan(res); err != nil { - res = nil - if err == memcache.ErrNotFound { - err = nil - return - } - } - if err != nil { - log.Errorv(c, log.KV("CacheNone", fmt.Sprintf("%+v", err)), log.KV("key", key)) - return - } - return -} - -// CacheString get data from mc -func (d *Dao) CacheString(c context.Context, id int64) (res string, err error) { - key := demoKey(id) - err = d.mc.Get(c, key).Scan(&res) - if err != nil { - if err == memcache.ErrNotFound { - err = nil - return - } - log.Errorv(c, log.KV("CacheString", fmt.Sprintf("%+v", err)), log.KV("key", key)) - return - } - return -} - -// AddCacheDemos Set data to mc -func (d *Dao) AddCacheDemos(c context.Context, values map[int64]*Demo) (err error) { - if len(values) == 0 { - return - } - for id, val := range values { - key := demoKey(id) - item := &memcache.Item{Key: key, Object: val, Expiration: d.demoExpire, Flags: memcache.FlagJSON} - if err = d.mc.Set(c, item); err != nil { - log.Errorv(c, log.KV("AddCacheDemos", fmt.Sprintf("%+v", err)), log.KV("key", key)) - return - } - } - return -} - -// AddCacheDemos2 Set data to mc -func (d *Dao) AddCacheDemos2(c context.Context, values map[int64]*Demo, tp int64) (err error) { - if len(values) == 0 { - return - } - for id, val := range values { - key := demo2Key(id, tp) - item := &memcache.Item{Key: key, Object: val, Expiration: d.demoExpire, Flags: memcache.FlagJSON} - if err = d.mc.Set(c, item); err != nil { - log.Errorv(c, log.KV("AddCacheDemos2", fmt.Sprintf("%+v", err)), log.KV("key", key)) - return - } - } - return -} - -// AddCacheDemo 这里也支持自定义注释 会替换默认的注释 -func (d *Dao) AddCacheDemo(c context.Context, id int64, val *Demo) (err error) { - if val == nil { - return - } - key := demoKey(id) - item := &memcache.Item{Key: key, Object: val, Expiration: d.demoExpire, Flags: memcache.FlagJSON | memcache.FlagGzip} - if err = d.mc.Set(c, item); err != nil { - log.Errorv(c, log.KV("AddCacheDemo", fmt.Sprintf("%+v", err)), log.KV("key", key)) - return - } - return -} - -// AddCacheDemo1 Set data to mc -func (d *Dao) AddCacheDemo1(c context.Context, id int64, val *Demo, mid int64) (err error) { - if val == nil { - return - } - key := keyMid(id, mid) - item := &memcache.Item{Key: key, Object: val, Expiration: d.demoExpire, Flags: memcache.FlagGOB} - if err = d.mc.Set(c, item); err != nil { - log.Errorv(c, log.KV("AddCacheDemo1", fmt.Sprintf("%+v", err)), log.KV("key", key)) - return - } - return -} - -// AddCacheNone Set data to mc -func (d *Dao) AddCacheNone(c context.Context, val *Demo) (err error) { - if val == nil { - return - } - key := noneKey() - item := &memcache.Item{Key: key, Object: val, Expiration: d.demoExpire, Flags: memcache.FlagJSON} - if err = d.mc.Set(c, item); err != nil { - log.Errorv(c, log.KV("AddCacheNone", fmt.Sprintf("%+v", err)), log.KV("key", key)) - return - } - return -} - -// AddCacheString Set data to mc -func (d *Dao) AddCacheString(c context.Context, id int64, val string) (err error) { - if len(val) == 0 { - return - } - key := demoKey(id) - bs := []byte(val) - item := &memcache.Item{Key: key, Value: bs, Expiration: d.demoExpire, Flags: memcache.FlagRAW} - if err = d.mc.Set(c, item); err != nil { - log.Errorv(c, log.KV("AddCacheString", fmt.Sprintf("%+v", err)), log.KV("key", key)) - return - } - return -} - -// DelCacheDemos delete data from mc -func (d *Dao) DelCacheDemos(c context.Context, ids []int64) (err error) { - if len(ids) == 0 { - return - } - for _, id := range ids { - key := demoKey(id) - if err = d.mc.Delete(c, key); err != nil { - if err == memcache.ErrNotFound { - err = nil - continue - } - log.Errorv(c, log.KV("DelCacheDemos", fmt.Sprintf("%+v", err)), log.KV("key", key)) - return - } - } - return -} - -// DelCacheDemo delete data from mc -func (d *Dao) DelCacheDemo(c context.Context, id int64) (err error) { - key := demoKey(id) - if err = d.mc.Delete(c, key); err != nil { - if err == memcache.ErrNotFound { - err = nil - return - } - log.Errorv(c, log.KV("DelCacheDemo", fmt.Sprintf("%+v", err)), log.KV("key", key)) - return - } - return -} - -// DelCacheDemo1 delete data from mc -func (d *Dao) DelCacheDemo1(c context.Context, id int64, mid int64) (err error) { - key := keyMid(id, mid) - if err = d.mc.Delete(c, key); err != nil { - if err == memcache.ErrNotFound { - err = nil - return - } - log.Errorv(c, log.KV("DelCacheDemo1", fmt.Sprintf("%+v", err)), log.KV("key", key)) - return - } - return -} - -// DelCacheNone delete data from mc -func (d *Dao) DelCacheNone(c context.Context) (err error) { - key := noneKey() - if err = d.mc.Delete(c, key); err != nil { - if err == memcache.ErrNotFound { - err = nil - return - } - log.Errorv(c, log.KV("DelCacheNone", fmt.Sprintf("%+v", err)), log.KV("key", key)) - return - } - return -} diff --git a/tool/kratos-gen-mc/testdata/model.pb.go b/tool/kratos-gen-mc/testdata/model.pb.go deleted file mode 100644 index fdf2ccfff..000000000 --- a/tool/kratos-gen-mc/testdata/model.pb.go +++ /dev/null @@ -1,328 +0,0 @@ -// Code generated by protoc-gen-gogo. DO NOT EDIT. -// source: model.proto - -/* - Package model is a generated protocol buffer package. - - It is generated from these files: - model.proto - - It has these top-level messages: - Demo -*/ -package testdata - -import proto "github.com/golang/protobuf/proto" -import fmt "fmt" -import math "math" -import _ "github.com/gogo/protobuf/gogoproto" - -import io "io" - -// Reference imports to suppress errors if they are not otherwise used. -var _ = proto.Marshal -var _ = fmt.Errorf -var _ = math.Inf - -// This is a compile-time assertion to ensure that this generated file -// is compatible with the proto package it is being compiled against. -// A compilation error at this line likely means your copy of the -// proto package needs to be updated. -const _ = proto.ProtoPackageIsVersion2 // please upgrade the proto package - -type Demo struct { - ID int64 `protobuf:"varint,1,opt,name=ID,proto3" json:"id"` - Title string `protobuf:"bytes,3,opt,name=Title,proto3" json:"title"` -} - -func (m *Demo) Reset() { *m = Demo{} } -func (m *Demo) String() string { return proto.CompactTextString(m) } -func (*Demo) ProtoMessage() {} -func (*Demo) Descriptor() ([]byte, []int) { return fileDescriptorModel, []int{0} } - -func init() { - proto.RegisterType((*Demo)(nil), "model.Demo") -} -func (m *Demo) Marshal() (dAtA []byte, err error) { - size := m.Size() - dAtA = make([]byte, size) - n, err := m.MarshalTo(dAtA) - if err != nil { - return nil, err - } - return dAtA[:n], nil -} - -func (m *Demo) MarshalTo(dAtA []byte) (int, error) { - var i int - _ = i - var l int - _ = l - if m.ID != 0 { - dAtA[i] = 0x8 - i++ - i = encodeVarintModel(dAtA, i, uint64(m.ID)) - } - if len(m.Title) > 0 { - dAtA[i] = 0x1a - i++ - i = encodeVarintModel(dAtA, i, uint64(len(m.Title))) - i += copy(dAtA[i:], m.Title) - } - return i, nil -} - -func encodeVarintModel(dAtA []byte, offset int, v uint64) int { - for v >= 1<<7 { - dAtA[offset] = uint8(v&0x7f | 0x80) - v >>= 7 - offset++ - } - dAtA[offset] = uint8(v) - return offset + 1 -} -func (m *Demo) Size() (n int) { - var l int - _ = l - if m.ID != 0 { - n += 1 + sovModel(uint64(m.ID)) - } - l = len(m.Title) - if l > 0 { - n += 1 + l + sovModel(uint64(l)) - } - return n -} - -func sovModel(x uint64) (n int) { - for { - n++ - x >>= 7 - if x == 0 { - break - } - } - return n -} -func sozModel(x uint64) (n int) { - return sovModel(uint64((x << 1) ^ uint64((int64(x) >> 63)))) -} -func (m *Demo) Unmarshal(dAtA []byte) error { - l := len(dAtA) - iNdEx := 0 - for iNdEx < l { - preIndex := iNdEx - var wire uint64 - for shift := uint(0); ; shift += 7 { - if shift >= 64 { - return ErrIntOverflowModel - } - if iNdEx >= l { - return io.ErrUnexpectedEOF - } - b := dAtA[iNdEx] - iNdEx++ - wire |= (uint64(b) & 0x7F) << shift - if b < 0x80 { - break - } - } - fieldNum := int32(wire >> 3) - wireType := int(wire & 0x7) - if wireType == 4 { - return fmt.Errorf("proto: Demo: wiretype end group for non-group") - } - if fieldNum <= 0 { - return fmt.Errorf("proto: Demo: illegal tag %d (wire type %d)", fieldNum, wire) - } - switch fieldNum { - case 1: - if wireType != 0 { - return fmt.Errorf("proto: wrong wireType = %d for field ID", wireType) - } - m.ID = 0 - for shift := uint(0); ; shift += 7 { - if shift >= 64 { - return ErrIntOverflowModel - } - if iNdEx >= l { - return io.ErrUnexpectedEOF - } - b := dAtA[iNdEx] - iNdEx++ - m.ID |= (int64(b) & 0x7F) << shift - if b < 0x80 { - break - } - } - case 3: - if wireType != 2 { - return fmt.Errorf("proto: wrong wireType = %d for field Title", wireType) - } - var stringLen uint64 - for shift := uint(0); ; shift += 7 { - if shift >= 64 { - return ErrIntOverflowModel - } - if iNdEx >= l { - return io.ErrUnexpectedEOF - } - b := dAtA[iNdEx] - iNdEx++ - stringLen |= (uint64(b) & 0x7F) << shift - if b < 0x80 { - break - } - } - intStringLen := int(stringLen) - if intStringLen < 0 { - return ErrInvalidLengthModel - } - postIndex := iNdEx + intStringLen - if postIndex > l { - return io.ErrUnexpectedEOF - } - m.Title = string(dAtA[iNdEx:postIndex]) - iNdEx = postIndex - default: - iNdEx = preIndex - skippy, err := skipModel(dAtA[iNdEx:]) - if err != nil { - return err - } - if skippy < 0 { - return ErrInvalidLengthModel - } - if (iNdEx + skippy) > l { - return io.ErrUnexpectedEOF - } - iNdEx += skippy - } - } - - if iNdEx > l { - return io.ErrUnexpectedEOF - } - return nil -} -func skipModel(dAtA []byte) (n int, err error) { - l := len(dAtA) - iNdEx := 0 - for iNdEx < l { - var wire uint64 - for shift := uint(0); ; shift += 7 { - if shift >= 64 { - return 0, ErrIntOverflowModel - } - if iNdEx >= l { - return 0, io.ErrUnexpectedEOF - } - b := dAtA[iNdEx] - iNdEx++ - wire |= (uint64(b) & 0x7F) << shift - if b < 0x80 { - break - } - } - wireType := int(wire & 0x7) - switch wireType { - case 0: - for shift := uint(0); ; shift += 7 { - if shift >= 64 { - return 0, ErrIntOverflowModel - } - if iNdEx >= l { - return 0, io.ErrUnexpectedEOF - } - iNdEx++ - if dAtA[iNdEx-1] < 0x80 { - break - } - } - return iNdEx, nil - case 1: - iNdEx += 8 - return iNdEx, nil - case 2: - var length int - for shift := uint(0); ; shift += 7 { - if shift >= 64 { - return 0, ErrIntOverflowModel - } - if iNdEx >= l { - return 0, io.ErrUnexpectedEOF - } - b := dAtA[iNdEx] - iNdEx++ - length |= (int(b) & 0x7F) << shift - if b < 0x80 { - break - } - } - iNdEx += length - if length < 0 { - return 0, ErrInvalidLengthModel - } - return iNdEx, nil - case 3: - for { - var innerWire uint64 - var start int = iNdEx - for shift := uint(0); ; shift += 7 { - if shift >= 64 { - return 0, ErrIntOverflowModel - } - if iNdEx >= l { - return 0, io.ErrUnexpectedEOF - } - b := dAtA[iNdEx] - iNdEx++ - innerWire |= (uint64(b) & 0x7F) << shift - if b < 0x80 { - break - } - } - innerWireType := int(innerWire & 0x7) - if innerWireType == 4 { - break - } - next, err := skipModel(dAtA[start:]) - if err != nil { - return 0, err - } - iNdEx = start + next - } - return iNdEx, nil - case 4: - return iNdEx, nil - case 5: - iNdEx += 4 - return iNdEx, nil - default: - return 0, fmt.Errorf("proto: illegal wireType %d", wireType) - } - } - panic("unreachable") -} - -var ( - ErrInvalidLengthModel = fmt.Errorf("proto: negative length found during unmarshaling") - ErrIntOverflowModel = fmt.Errorf("proto: integer overflow") -) - -func init() { proto.RegisterFile("model.proto", fileDescriptorModel) } - -var fileDescriptorModel = []byte{ - // 166 bytes of a gzipped FileDescriptorProto - 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xe2, 0xe2, 0xce, 0xcd, 0x4f, 0x49, - 0xcd, 0xd1, 0x2b, 0x28, 0xca, 0x2f, 0xc9, 0x17, 0x62, 0x05, 0x73, 0xa4, 0x74, 0xd3, 0x33, 0x4b, - 0x32, 0x4a, 0x93, 0xf4, 0x92, 0xf3, 0x73, 0xf5, 0xd3, 0xf3, 0xd3, 0xf3, 0xf5, 0xc1, 0xb2, 0x49, - 0xa5, 0x69, 0x60, 0x1e, 0x98, 0x03, 0x66, 0x41, 0x74, 0x29, 0x39, 0x71, 0xb1, 0x3b, 0x16, 0x95, - 0x64, 0x26, 0xe7, 0xa4, 0x0a, 0x89, 0x71, 0x31, 0x79, 0xba, 0x48, 0x30, 0x2a, 0x30, 0x6a, 0x30, - 0x3b, 0xb1, 0xbd, 0xba, 0x27, 0xcf, 0x94, 0x99, 0x12, 0xc4, 0xe4, 0xe9, 0x22, 0x24, 0xcf, 0xc5, - 0x1a, 0x92, 0x59, 0x92, 0x93, 0x2a, 0xc1, 0xac, 0xc0, 0xa8, 0xc1, 0xe9, 0xc4, 0xf9, 0xea, 0x9e, - 0x3c, 0x6b, 0x09, 0x48, 0x20, 0x08, 0x22, 0xee, 0x24, 0x71, 0xe2, 0xa1, 0x1c, 0xc3, 0x85, 0x87, - 0x72, 0x0c, 0x27, 0x1e, 0xc9, 0x31, 0x5e, 0x78, 0x24, 0xc7, 0xf8, 0xe0, 0x91, 0x1c, 0xe3, 0x8c, - 0xc7, 0x72, 0x0c, 0x49, 0x6c, 0x60, 0x4b, 0x8c, 0x01, 0x01, 0x00, 0x00, 0xff, 0xff, 0x11, 0xa6, - 0xfa, 0x1c, 0xa9, 0x00, 0x00, 0x00, -} diff --git a/tool/kratos-gen-mc/testdata/model.proto b/tool/kratos-gen-mc/testdata/model.proto deleted file mode 100644 index fdc58a5cc..000000000 --- a/tool/kratos-gen-mc/testdata/model.proto +++ /dev/null @@ -1,14 +0,0 @@ -syntax = "proto3"; -package testdata; -import "github.com/gogo/protobuf/gogoproto/gogo.proto"; - -option (gogoproto.goproto_enum_prefix_all) = false; -option (gogoproto.goproto_getters_all) = false; -option (gogoproto.unmarshaler_all) = true; -option (gogoproto.marshaler_all) = true; -option (gogoproto.sizer_all) = true; - -message Demo { - int64 ID = 1 [(gogoproto.jsontag) = "id"]; - string Title = 3 [(gogoproto.jsontag) = "title"]; -} \ No newline at end of file diff --git a/tool/kratos-gen-project/main-packr.go b/tool/kratos-gen-project/main-packr.go deleted file mode 100644 index 6766d9531..000000000 --- a/tool/kratos-gen-project/main-packr.go +++ /dev/null @@ -1,8 +0,0 @@ -// +build !skippackr -// Code generated by github.com/gobuffalo/packr/v2. DO NOT EDIT. - -// You can use the "packr clean" command to clean up this, -// and any other packr generated files. -package main - -import _ "github.com/go-kratos/kratos/tool/kratos-gen-project/packrd" diff --git a/tool/kratos-gen-project/main.go b/tool/kratos-gen-project/main.go deleted file mode 100644 index 10686c885..000000000 --- a/tool/kratos-gen-project/main.go +++ /dev/null @@ -1,75 +0,0 @@ -package main - -import ( - "os" - "strings" - - "github.com/urfave/cli/v2" -) - -var appHelpTemplate = `{{if .Usage}}{{.Usage}}{{end}} - -USAGE: - kratos new {{if .UsageText}}{{.UsageText}}{{else}}{{.HelpName}} {{if .VisibleFlags}}[global options]{{end}}{{if .Commands}} command [command options]{{end}} {{if .ArgsUsage}}{{.ArgsUsage}}{{else}}[arguments...]{{end}}{{end}}{{if .Version}}{{if not .HideVersion}} - -VERSION: - {{.Version}}{{end}}{{end}}{{if .Description}} - -DESCRIPTION: - {{.Description}}{{end}}{{if len .Authors}} - -AUTHOR{{with $length := len .Authors}}{{if ne 1 $length}}S{{end}}{{end}}: - {{range $index, $author := .Authors}}{{if $index}} - {{end}}{{$author}}{{end}}{{end}}{{if .VisibleCommands}} - -OPTIONS: - {{range $index, $option := .VisibleFlags}}{{if $index}} - {{end}}{{$option}}{{end}}{{end}}{{if .Copyright}} - -COPYRIGHT: - {{.Copyright}}{{end}} -` - -func main() { - app := cli.NewApp() - app.Name = "" - app.Usage = "kratos 新项目创建工具" - app.UsageText = "项目名 [options]" - app.HideVersion = true - app.CustomAppHelpTemplate = appHelpTemplate - app.Flags = []cli.Flag{ - &cli.StringFlag{ - Name: "d", - Value: "", - Usage: "指定项目所在目录", - Destination: &p.path, - }, - &cli.BoolFlag{ - Name: "http", - Usage: "只使用http 不使用grpc", - Destination: &p.onlyHTTP, - }, - &cli.BoolFlag{ - Name: "grpc", - Usage: "只使用grpc 不使用http", - Destination: &p.onlyGRPC, - }, - &cli.BoolFlag{ - Name: "proto", - Usage: "废弃参数 无作用", - Destination: &p.none, - Hidden: true, - }, - } - if len(os.Args) < 2 || strings.HasPrefix(os.Args[1], "-") { - app.Run([]string{"-h"}) - return - } - p.Name = os.Args[1] - app.Action = runNew - args := append([]string{os.Args[0]}, os.Args[2:]...) - err := app.Run(args) - if err != nil { - panic(err) - } -} diff --git a/tool/kratos-gen-project/new.go b/tool/kratos-gen-project/new.go deleted file mode 100644 index c62349866..000000000 --- a/tool/kratos-gen-project/new.go +++ /dev/null @@ -1,61 +0,0 @@ -package main - -import ( - "fmt" - "io/ioutil" - "os" - "path/filepath" - "strings" - - common "github.com/go-kratos/kratos/tool/pkg" - - "github.com/urfave/cli/v2" -) - -func runNew(ctx *cli.Context) (err error) { - if p.onlyGRPC && p.onlyHTTP { - p.onlyGRPC = false - p.onlyHTTP = false - } - if p.path != "" { - if p.path, err = filepath.Abs(p.path); err != nil { - return - } - p.path = filepath.Join(p.path, p.Name) - } else { - pwd, _ := os.Getwd() - p.path = filepath.Join(pwd, p.Name) - } - p.ModPrefix = strings.ReplaceAll(modPath(p.path), "\\", "/") - // creata a project - if err := create(); err != nil { - return err - } - fmt.Printf("Project: %s\n", p.Name) - fmt.Printf("OnlyGRPC: %t\n", p.onlyGRPC) - fmt.Printf("OnlyHTTP: %t\n", p.onlyHTTP) - fmt.Printf("Directory: %s\n\n", p.path) - fmt.Println("项目创建成功.") - return nil -} - -func modPath(p string) string { - dir := filepath.Dir(p) - for { - if _, err := os.Stat(filepath.Join(dir, "go.mod")); err == nil { - content, _ := ioutil.ReadFile(filepath.Join(dir, "go.mod")) - mod := common.RegexpReplace(`module\s+(?P[\S]+)`, string(content), "$name") - name := strings.TrimPrefix(filepath.Dir(p), dir) - name = strings.TrimPrefix(name, string(os.PathSeparator)) - if name == "" { - return fmt.Sprintf("%s/", mod) - } - return fmt.Sprintf("%s/%s/", mod, name) - } - parent := filepath.Dir(dir) - if dir == parent { - return "" - } - dir = parent - } -} diff --git a/tool/kratos-gen-project/packrd/packed-packr.go b/tool/kratos-gen-project/packrd/packed-packr.go deleted file mode 100644 index 4ab920e93..000000000 --- a/tool/kratos-gen-project/packrd/packed-packr.go +++ /dev/null @@ -1,230 +0,0 @@ -// +build !skippackr -// Code generated by github.com/gobuffalo/packr/v2. DO NOT EDIT. - -// You can use the "packr2 clean" command to clean up this, -// and any other packr generated files. -package packrd - -import ( - "github.com/gobuffalo/packr/v2" - "github.com/gobuffalo/packr/v2/file/resolver" -) - -var _ = func() error { - const gk = "23f256f8ae67626d12a89a2afb9fd55c" - g := packr.New(gk, "") - hgr, err := resolver.NewHexGzip(map[string]string{ - "01947c6dd922af83263605f0243198da": "1f8b08000000000000ff8c51cb8e1331103cdb5fd1cc616547c1be202e2807362b7121e1fd013d7ead158f9d783a246834ff8e1c061471401cac96dc5d55dd554734070c0e2c16cee3702c954070d69992c95da9e39c75d3a476c57eaccec7eb3c4f93dae3e0e659c74cae664c7a28d6a58eb32e447a3ef7ca944187f2f25091caa897723c046d4af6fa88096dccff336f91b0c7d1e9f1943a2e39f7e76c60ef2e4f8f4282b03dacc653524f8f6b301e5a53c835b85adb2b55c2c4d977aced22667c8036bc2dd9c7d03e089655d4d70fbbf79c49cea2bfa1377f3aef1c89cef68aca903aa9bee501ebf88c493c18926f6ec32f3690636a52ac3a3ad7ccd97cc764e817c9364597e99ea3a98a07e3c33f896c0f9bdbe27b77d9fdf8f2698170663c6c969b61b2bddaa6323a2167fe1b3d2f7e090b2b8b45c267bcbcad144d72c2d015968c9b23adae215a88995ebf9220b012ac6eb9aa05f2b7af5a4370042d22f0b50c60fb3be59f010000ffff90ef4f8158020000", - "07b6987724e01017288e376e48d717a4": "1f8b08000000000000ff2a484cce4e4c4f55c8cd4f49cde1e2d2d757f02e4a2cc92f56c848cdc9c957c80673f4b84a2a0b526132c52545a5c9250ad55c9c1e6035c525459979e95cb55c10558e452599c939a948ca3c5d1432f34acc4cb8389df3f34a52f34a605a381d4b4b32f28be02600020000ffffcb8696638d000000", - "0800ba3c38cf5e9bf163d71215211839": "1f8b08000000000000ff94924d6fdb300c86cfd2af200c2c90324fbe17c865ed801dda2c6856ecacc8b423d49654594e1a18feef83fcd1a6cbba8f8325987a45be8f4827d5a32c111af407ad90525d3beb03304a12654dc0e790509214754828256e0749d7893b9b6f3c16fab9efbb4eac658d7d9f49a7a3f2bd636d027a23ab2c9736ea4a1df6ed4e285b67a5fdf4e865b04d366deeb1cc943545e66425736d62e9b7172a69cacc791becae2d32174e0e9b0c6b174e17b96d596176d41e13ca293d480f1b6f0f3a470f2b8871b1c6e316035be3311d039fb5c999c123733b7183b5dda23fa0e729c4d8723b3e15e79cd22c83e9777e4141a399d768f0ad0ad0512215c072021277d251924b0bb9b4e2465ada0fb9d6788c3540ced9409a1c3c86d61b418bd6a82861f97c8d036b603694822a206a184f01bd8f9ff53cd66e60058b49d65142a4ba0258cc66be7fbbbbedfa9492e8e80af294929e1255c00a1a715dd9062989f95630dff82183dab3443a57692583b646045b57490a8d908a53325a9ea8b6f2f415abca42e99d821c6b3bd89c80ce08f88b94a9f00cd3fc89eb714fc1e3132cdd4e0c927b7ce2c03cbaea04cba1f5e24b5c7f651f15aba17767324e495107b1f1da848225fbc1e0872619aa0c43fb1ec6c3fd2decea7fe378b8bffd5f94d760e3de63599c8b624387bc265cc10492c0c7178eb19d97b0adaffe0cbcd1a6041797b047f0d8d8d6c701bfc48dcadf73e29bde706017ad7a418b95617176dcf5719ee2a8cff9f9e46c184a50c3fa376f8396c51a3dfd190000ffff35636a08ee040000", - "09569e3bf810225e525835d677053b1b": "1f8b08000000000000ff5256702c2dc9c82fe25256084a2dcb4c2d4f2de202040000ffffbe75c21514000000", - "0a6bc3e72bc7017e18eb65cbe3145be8": "1f8b08000000000000ff9490b16eeb300c45e7f02b084ff67b86b4774bb70e0d32f407149991d9d8a240d34901c3ff5e38e9d031dd84ab7301dee33dfe3fcd3c74786325ce9f140dbcc78f9ef0915b4838860b4d38cd4a683de164f30979c22c76870c39df3fce9cc3f0e8398012e22524c28e01782ca28635ecaa6571efd21d95cefcb5aecbe20e61a475f59c8d3487c17741aa67b889f4ca919e66497dd212ffc2f766a502d85589ad9f4f2ecae893481ac86fbe2a6800bc4ff29228930623bc683099d0ee3ee13ce7886f996d5f4add60fd6f5f4a8b5b5a372d92aa68830bec4ac81cebade25e377b7517c41d55aedc91b6f8b3f457b21de60e746b719bf4781de8b62fa5696085ef000000ffffe36b7bafd7010000", - "0a985fe02d21f9d997ee7c01e1f60f82": "1f8b08000000000000ff9452c18adb30103d6bbe626a285890b5e935740fa5b4dd1c36a44d722e5a79ac0c912523c97417e37f2f52c2b2813db4175b9a79cfefcd1b8f4a9f95211c143b001e461f12d620aade2a5381a87cbc3cdbc8c6299b2ff1256a65cb31f1401580a8e6b979f4dd2e50cfcfcb32cfcd560db42c2dbb44c129db769cf186d3697a6ab41f5ae3efce41251fdbeb6b3c9b567bd7b7a3b2aa63f72f78eb4d0512a09f9c2e33d4126710d97cb35321522d41586f9a8de3543bb612db163b7a9a0c66d01a73b3e3703f8f2a9d16101df5144af5abf56ff9bdafabd7b93026155225415ccd5e042488df2bd499f87d727a851402aeefb1e3d2ff328e19c27da97fb847c736db15a372ac6b0a41825840e8cc19d4996a7d520e7d6cf625fc157e92202e8b68b63e71ff52eb155ef7d1ec373f1e8ebb9bfbcfe3e67053387cfbf57853d86c0f1244ef433112b3f2e73b0de2cdd486122abcc8e2c758ad3036fb14d8995aca4cfac3499f30962f6815e9bf1dac4108f11a5bce48bc9b3a3d73095d88fce3357b4b34d6972369efbad20a94a6e0deb1f270dc65a18e7a35d9b4bec12e39f805fe060000ffff29d8de170f030000", - "0babbb2bdef8b27731b8c6e569f74aa9": "1f8b08000000000000ff5256702c2dc9c82fe25256084a2dcb4c2d4f2de202040000ffffbe75c21514000000", - "0c5ba5fa8837f2def63c05cc79c53394": "1f8b08000000000000ff248eb16eeb300c45e7f02b2e3249ef05d6de31f5da0e457e80b61959b52d1a120d0f45ffbdb03391e0bd073c21e07fb7a579c09e8aa4fc2dbd5108788c82d7dd3862e1492aea5604360aaa6d1d5245563b4b8694cfe09932cf2fae215ab99f380a0656a2b4ac5a0c8e2ed7986cdcbaa6d72544d5384b387e5fc9138510f52d4a96c226980a9b5698ea7cead173cb3db2ec0fa9d6b23a0ff76f60bde1089cbf414ad1e2f143979573eadd4135f7c3c765d9dba3fa297b7b3fc7970ca99edbc7bbf7f44b7f010000ffff3c96d9e80d010000", - "17e45e0f3497be1d7b615abf452cafa3": "1f8b08000000000000ff4ccc41cac2400c05e0f5e414610e5092fefc28822bafe04e5c0c4d16814ea74ca35e5fa22ebacbfb1e2fb7cb6cbaf81dd252aae219b3686d19d2da9bb7c83ead195211e991783c0c34d0c027e691399ac9ed19cb7f826432c7c90449accc57abda1efe1912d52d43ea5a64c7e38f5fdd5c77fef77544c4f8b96b8eb46578070000ffff6f600d93b7000000", - "1a41024e60169dfd892e57d36264a7e3": "1f8b08000000000000ff2a484cce4e4c4f55c8cd4f49cde1e2d2d757f02e4a2cc92f56c848cdc9c957c80673f4b84a2a0b526132c52545a5c9250ad55c9c1e6035c525459979e95cb55c10558e452599c939a948ca3c5d1432f34acc4cb8389df3f34a52f34a605a381d4b4b32f28be02600020000ffffcb8696638d000000", - "1dc54a258d1b22873bd8a065ef5f5136": "1f8b08000000000000ff4ccc41ca02310c05e075738ad2030c497f7e14c1955770272eca248bc0743a74a25e5fa22ebacbfb1e2fb7cba2b2da1dc25aaac4734c2cb525085b6fd63cdbbc250885b97ba27c9870c2894e99329137b3e9d397ff084179f19310026b59ae5aa53dec3344ac7b82d0a5f0c0f9c7afae2683ff7d3dc618fde7d01c714ff00e0000ffff7dc54fc8b7000000", - "21390808875e3972b5fb30ef533f7595": "1f8b08000000000000ff8a0e4e2d2a4b2d8ae552505050484c492952b0555032d003432b0b03030325b04c49666e6a7e690948d2b058890b100000ffffb7699cdc36000000", - "240e2e99283e50af1f153a929e3d1116": "1f8b08000000000000ff64cdc14ec3300cc6f1337e0a1fe150274e18092fc13b646d3013cd3cb264429af6ee889603d54e3ef8f7d75774ea73c6eb95de52c9b71b802832b107a8f9ab1f6ac64740449443fbe87b1ab518d1e1b3a6a667b31e2ce9dc72bd67a2e654b5e9bebfe385c911ff9a878d99d35136ca935bd5f222ad62becd3137bc58b2640767f9d5beb8c871e79f77c394d88731c4c053f8eb54e64cff72a9a771d98fc4f004f0130000ffff1f9ac7b6f4000000", - "28edc15b141434022bafc70e865e28c3": "1f8b08000000000000ff8c51cb8e1331103cdb5fd1cc616547c1be202e2807362b7121e1fd013d7ead158f9d783a246834ff8e1c061471401cac96dc5d55dd554734070c0e2c16cee3702c954070d69992c95da9e39c75d3a476c57eaccec7eb3c4f93dae3e0e659c74cae664c7a28d6a58eb32e447a3ef7ca944187f2f25091caa897723c046d4af6fa88096dccff336f91b0c7d1e9f1943a2e39f7e76c60ef2e4f8f4282b03dacc653524f8f6b301e5a53c835b85adb2b55c2c4d977aced22667c8036bc2dd9c7d03e089655d4d70fbbf79c49cea2bfa1377f3aef1c89cef68aca903aa9bee501ebf88c493c18926f6ec32f3690636a52ac3a3ad7ccd97cc764e817c9364597e99ea3a98a07e3c33f896c0f9bdbe27b77d9fdf8f2698170663c6c969b61b2bddaa6323a2167fe1b3d2f7e090b2b8b45c267bcbcad144d72c2d015968c9b23adae215a88995ebf9220b012ac6eb9aa05f2b7af5a4370042d22f0b50c60fb3be59f010000ffff90ef4f8158020000", - "2b96476f98cc5a0058f5fdd12ab432e1": "1f8b08000000000000ff5cccc10ac2300cc6f173f214230f505a058782275fc19b78084b0e81761d5dd4d7970ac2d8f1ff83ef7bdcb2e9ec4f84998b0ed781444b2584a555afbd7d5a0881455aaf7418430c31a4cbe9389e09c124f7598a083cb9bdff21c6f96e45ebcb7fbb95109ab2ecedd3cc758ffd746b7125fc060000ffffe2756aadaa000000", - "2bec6c6ed25a4c6dcdb30f32376e8814": "1f8b08000000000000ff94924d6fdb300c86cfd2af200c2c90324fbe17c865ed801dda2c6856ecacc8b423d49654594e1a18feef83fcd1a6cbba8f8325987a45be8f4827d5a32c111af407ad90525d3beb03304a12654dc0e790509214754828256e0749d7893b9b6f3c16fab9efbb4eac658d7d9f49a7a3f2bd636d027a23ab2c9736ea4a1df6ed4e285b67a5fdf4e865b04d366deeb1cc943545e66425736d62e9b7172a69cacc791becae2d32174e0e9b0c6b174e17b96d596176d41e13ca293d480f1b6f0f3a470f2b8871b1c6e316035be3311d039fb5c999c123733b7183b5dda23fa0e729c4d8723b3e15e79cd22c83e9777e4141a399d768f0ad0ad0512215c072021277d251924b0bb9b4e2465ada0fb9d6788c3540ced9409a1c3c86d61b418bd6a82861f97c8d036b603694822a206a184f01bd8f9ff53cd66e60058b49d65142a4ba0258cc66be7fbbbbedfa9492e8e80af294929e1255c00a1a715dd9062989f95630dff82183dab3443a57692583b646045b57490a8d908a53325a9ea8b6f2f415abca42e99d821c6b3bd89c80ce08f88b94a9f00cd3fc89eb714fc1e3132cdd4e0c927b7ce2c03cbaea04cba1f5e24b5c7f651f15aba17767324e495107b1f1da848225fbc1e0872619aa0c43fb1ec6c3fd2decea7fe378b8bffd5f94d760e3de63599c8b624387bc265cc10492c0c7178eb19d97b0adaffe0cbcd1a6041797b047f0d8d8d6c701bfc48dcadf73e29bde706017ad7a418b95617176dcf5719ee2a8cff9f9e46c184a50c3fa376f8396c51a3dfd190000ffff35636a08ee040000", - "2c5e0d8471a941cbc1b0532422997425": "1f8b08000000000000ff8c90316ec3300c45e7f0148427bb35a4bd5bba756890a117506c4666638b024527050cdfbd70d2a163d6c7f781cfef3dbe9e661e7bbcb112a76fea0cbcc7af81f0c12d449cc2850a9659096d202c369f900b26b1bb64c8e97e38730ae323e70072e82e2112f60cc0531635ac61572d8bfb94fea874e69f755d16770813adabe764a4298cbe0f523de315d22b77f4b44bea07b35c01ecaac836cc27d7c9e4a3481cc96fff57d000781fe52d52220d4678d16052d0eefbc0794e1d7e24b67dce7583f5cb3ee716375a372d92aa68830bec7248dcd55bc4bd6f6bd47d107754b9724fdae25ff37f642be60e746bf140b77dce4d03eb6f000000ffff14b8957d9c010000", - "2d07c6785bf36355d22a324ee7064328": "1f8b08000000000000ffbc91bd4a04311485ebe429428a416118333fa82c04052dc546bbc5229b5cd9c0fc7973e32ac3bcbb44071dabadb4ca395fc2e1836c6f5a0f3d3d71669c43a1852cab8b4215aa283775adce25672ef4894f3100ce9b6934211c0674f335d9f1e4f7ebd3b3c919323b1360be22dfc1104997214330eef1a71ed013acfa68307c764d18216b07abef066bdacceed30de948cf97ddaec9d329394b73b70ff7428bed11adeaffb4f2232af59fabe48b8f4cbf69c9bf82d0a2529c79d7a6582e71d9155a367bc9d94b047cff6642564a754172066f6057b85e30a1e957b8f9c21f010000ffff57d7944148020000", - "2e6074098bbcae09bb1bbbaf5c9c65d7": "1f8b08000000000000ff8c90416bdc301085cf9a5f31ab931436f2bdc187a609edc2064a937b50e4b157d8968c24275bc2fef7328b5a96b69482f19899379fe7bdc5bad10e849d8d007e5e622aa840481743a1639120643fd9816bccfc2e948b0f83041072f0e5b0be1817e76688d763b225e6a696651c1a1743df2c76b29d0ff23ff495dd4cde1d246880579bb0c32b3e8e3f5d39628bf534736bdd38a4b8864e69807e0d0e9f289707eb839af1aac2cc83c677106cc23c5251ecac975b94c634c69c7f29f5e5fcb761d3453752ba76715e6226f3ddced3af85af3665521a44e7b37d9968efdd013fb418b3f94c85c2ab9277bbc78fb7fbfbe7fdeed317a971d3a294207c8f9bcb9d7710dca394789dfdf331eba2f4cdb9b96931f8e92c138b0dde294a49831027e087c36119a51413fc24b558b337bbe0cb5f5097a44a713d7292ecc9f7d86dd1f5db0a0bf4c6f9ded9a8f4cdbf49890afb98cdb73530caf515f887e967ac769fc8a62ebe9de52710319bfba32f2a51d170821f010000ffffa3cee358a7020000", - "2f0ce61d7311ba883ba12633891e37a5": "1f8b08000000000000ff549131aed4301086ebcc29064b4809cadafd4a5b2c9be6356f57c0058c7762ac4d3c96336141e815749414f420710cae8378d74089173d515933feff6fc6bf937517eb096d0a10c6c459b0864a398e421f4441a5fa511440a57c9077f35bed78349e37976c8527733bd2c59b48627272e66af39962b130fb81b4e7c146af397be373720a1a0063709fd25d871a1cc7496ed50ed59b6377dce2beeb707f3ae15da756f13d5d0f43a02818e98a0b06dd5a433f47f7745dbbdee38bb2832ead03c73ef81639c9845aebc5acbb60876392c0b1c1baa3918bb645ca9973839fa02a7cdceef086fb6f48e169ad1ba89c5b7d8bb498567c7d0b51bfb4eee233cff15c372df6a3e8d72987287dadce6172fc9ef2c7ad3167eaed3c88793ea9b6c4d13450857e453fdb610cc3b2569549e61c97729d0ad503fcebddd3f5e92db5734dbbc8e0618df0f1dbf73f5fbea27f753ae0ef5f3f1f7f7c06633c6f3d45ca5608cb67a2300f98320b3bdc6cd6a86d0a7aedc0df000000ffff6187772e30020000", - "344d3e90de055e3f730ec93c8d518442": "1f8b08000000000000ffa492516bdb3010c7dff3290ebde42572e384259b200f657d5cc9d614c6564250e55b7cd4d279d2c550d8871f72dd242bdda0ece5eefcf3dfe7fbeb04d0614cc4c1809a174b350248183b7298cc0800a0ba7fca00e4ed1e0df8c7f4b331ef8ac5805b8e929e351acaf97cba30390c0843479183c72027d5f5b7cd974fbb9bf5fa76f7f972b3f9babeb95a4566390a6ebfaf2e13d98b4d6dc3beb634bce8b839783cfb5d612e2a760f183506898f2d53104d81a4ba2faa41e5d87b1b2a03770300186bed6a1bad138c3aa1e86c1ae3ea203fde8f277fc8b869ac108773c9ee10c871853b47e341bc1d728db691dad5e81eccb18f60120377eae3f5959a80ea8fd0569e8282896a29ec33d5ba8d2cecb859896bd5f6f43579e48318984dd31152108c9d6d0c942718512261caca11c0e80955945eacb067afadaf5fe062befc60727883a3bea3760da9c1cff69541cfc73f7afaf7f41ebdb3aec6eae5257ce67f73312b676569ca1cdf60035dcd3927b19272f14b4d005470b92e67cb625a4c8bb27fc89dffdbe5ef000000ffff82fe973e7e030000", - "34564973d460772fafff3026df19a783": "1f8b08000000000000ff8a0e4e2d2a4b2d8ae552505050484c492952b0555032d003432b4b03030325b04c49666e6a7e690948d2b058890b100000ffffb9f9177936000000", - "360acf295547cde8b9a14a8d71f86bde": "1f8b08000000000000ff8a0e4e2d2a4b2d8ae552505050484c492952b0555032d003432b0b03030325b04c49666e6a7e690948d2b058890b100000ffffb7699cdc36000000", - "3deb9e59e02e8c5bc6ba59a66141508f": "1f8b08000000000000ff8a0e4e2d2a4b2d8ae552505050484c492952b0555032d003432b0b03030325b04c49666e6a7e690948d2b058890b100000ffffb7699cdc36000000", - "3fcb8456a23b643f9cd2b5df95e3e497": "1f8b08000000000000ffe2525608c9c82c56c82c56485408f1f7f55148c94f2ecd4dcd2bd15370cacfcfade34a49cdcd77ad28c82c4a55b055503232c950e202040000ffffc36ed62935000000", - "450e380f3fbd9366ee660538f5f4eb47": "1f8b08000000000000ffe2525608c9c82c56c82c56485408f1f7f55148c94f2ecd4dcd2bd15370cacfcfade34a49cdcd77ad28c82c4a55b055503232c950e202040000ffffc36ed62935000000", - "45b4b7a3d09ea9195265478d55919d04": "1f8b08000000000000ff5256567049cdcde7e2525656562833d433d033e032d45378b2a3ebf9aefd4fbbe6bf68de5b5151c105080000ffff79fbc41327000000", - "47c5a1821e7054758d52c8004a7033de": "1f8b08000000000000ff9c90314bc3401cc5f77c8aff96045c0ae2521c62724a30b994f46ee8943bd3030f1b2bc9298e6eba281d8a1d74d04941280e226a07bf4c9bda6f21676b23bab9dc71efeefddebd7f9a0bae04b4b9e23bbc10b09773d52d92b6c8ba75e3f0b760b831720802e26c040818cf954c3ba2606019004cb619c87d65d56a3650dcf4b730f2004704300d02702889121fbb310a1126e046e1d76e8e5f46b3fed0f7cc15cd5052750483239ea7bb3cb7d656ed8ab0b44c7ae7d3bbc7b9215332130cf45a289e1d54cf3db4e9d080804be3186192103f444de2840d8830d086a78bfcbd5b8694d72793dec5f87d58f65fcbc1f36cf0340f4cff1158fdfcec6a327afb816bc47ee8c42dd8462db0f4046dadea1393c7c9a29cb568691bf6376add2c2f4fa70f371fb7f766ddf80c0000ffff632acdfbc6010000", - "47f7fee79587764108735a0e37a3f1cb": "1f8b08000000000000ff9452c18adb30103d6bbe626a285890b5e935740fa5b4dd1c36a44d722e5a79ac0c912523c97417e37f2f52c2b2813db4175b9a79cfefcd1b8f4a9f95211c143b001e461f12d620aade2a5381a87cbc3cdbc8c6299b2ff1256a65cb31f1401580a8e6b979f4dd2e50cfcfcb32cfcd560db42c2dbb44c129db769cf186d3697a6ab41f5ae3efce41251fdbeb6b3c9b567bd7b7a3b2aa63f72f78eb4d0512a09f9c2e33d4126710d97cb35321522d41586f9a8de3543bb612db163b7a9a0c66d01a73b3e3703f8f2a9d16101df5144af5abf56ff9bdafabd7b93026155225415ccd5e042488df2bd499f87d727a851402aeefb1e3d2ff328e19c27da97fb847c736db15a372ac6b0a41825840e8cc19d4996a7d520e7d6cf625fc157e92202e8b68b63e71ff52eb155ef7d1ec373f1e8ebb9bfbcfe3e67053387cfbf57853d86c0f1244ef433112b3f2e73b0de2cdd486122abcc8e2c758ad3036fb14d8995aca4cfac3499f30962f6815e9bf1dac4108f11a5bce48bc9b3a3d73095d88fce3357b4b34d6972369efbad20a94a6e0deb1f270dc65a18e7a35d9b4bec12e39f805fe060000ffff29d8de170f030000", - "4bafcbfe7d0faa99ecff3bd27c22d83f": "1f8b08000000000000ff5256567049cdcde7e2525656562833d433d033e032d45378b2a3ebf9aefd4fbbe6bf68de5b5151c105080000ffff79fbc41327000000", - "4f6064f875a8245d8fccaa9a7fa8b27c": "1f8b08000000000000ff5492b18ed4301086ebcc530c96901294d8fd4a5b2c9be69adb15f0023eaf63ac4d3c96336141e80a3a4a0a7a90780c5e0771af81622f5a5d15cde4ffbf19ff76d4e6ac9d451d3df8295262aca1128602db8f2ca012c3c402a012cef3fbe5411a9a94a3ee9c34d3acae9f78762a5856291a75d1e96443b110b9d14a47a30e4e5272caa5680434004ae12ec6bb1e25180a335fab2d8a7787feb0c15ddfe3ee78c4bb5e64f1bdbdec476f0363b0175c3168720dc312cced776d0687afca0eb2b4f61406ef5aa4c8334a2957b3ecbd1e0f913d8506ebde4e54b42dda942835f819aac2c7cd16afb867430a4f4ad940654cf6add262caf8fa1aa27cadcdd9255ac2a96e5a1c26966f63f281875a9cfc6ce8834d9f364a9deca09791d5cb59b4258ea681ca0f19fd628bc18feb5a55b2bca4b096792a548ff0bf776f2fb7b3d4c634ed2a83c71ce1d3f71f7fbf7e43f7e6b8c73fbf7f3dfdfc024a39da381b6cd26cb15c2632d188311193c1aecb5177ddc3b43e1199dbf02f0000ffff98f7b62e35020000", - "50c9e9a3056e1c5519a3545096c6cd2e": "1f8b08000000000000ff4ccc41ca02310c05e075738ad2030c497f7e14c1955770272eca248bc0743a74a25e5fa22ebacbfb1e2fb7cba2b2da1dc25aaac4734c2cb525085b6fd63cdbbc250885b97ba27c9870c2894e99329137b3e9d397ff084179f19310026b59ae5aa53dec3344ac7b82d0a5f0c0f9c7afae2683ff7d3dc618fde7d01c714ff00e0000ffff7dc54fc8b7000000", - "519277b78f6f840309ab0251f7b54419": "1f8b08000000000000ff8a0e4e2d2a4b2d8ae552505050484c492952b0555032d003432b4b03030325b04c49666e6a7e690948d2b058890b100000ffffb9f9177936000000", - "56df8426de024acea28c177fdbae2d79": "1f8b08000000000000ff8c90416bdc301085cf9a5f31ab931436f2bdc187a609edc2064a937b50e4b157d8968c24275bc2fef7328b5a96b69482f19899379fe7bdc5bad10e849d8d007e5e622aa840481743a1639120643fd9816bccfc2e948b0f83041072f0e5b0be1817e76688d763b225e6a696651c1a1743df2c76b29d0ff23ff495dd4cde1d246880579bb0c32b3e8e3f5d39628bf534736bdd38a4b8864e69807e0d0e9f289707eb839af1aac2cc83c677106cc23c5251ecac975b94c634c69c7f29f5e5fcb761d3453752ba76715e6226f3ddced3af85af3665521a44e7b37d9968efdd013fb418b3f94c85c2ab9277bbc78fb7fbfbe7fdeed317a971d3a294207c8f9bcb9d7710dca394789dfdf331eba2f4cdb9b96931f8e92c138b0dde294a49831027e087c36119a51413fc24b558b337bbe0cb5f5097a44a713d7292ecc9f7d86dd1f5db0a0bf4c6f9ded9a8f4cdbf49890afb98cdb73530caf515f887e967ac769fc8a62ebe9de52710319bfba32f2a51d170821f010000ffffa3cee358a7020000", - "5700a76799052e1b831d6826b3f89cc6": "1f8b08000000000000ff9c544d8fdb36103d93bf62220486b4b049a42d7a50a143b36a8b005d77d1b4e780224732b114a95054ecc0d07f2f48c9bb7151a4467c10c9e1703ede7be341c827d12128e128d5fde07c809c924c3a1bf014324ab2a07bcc2825d9f9cc1e9c7af4d8ead33c9fcf6c2f7a9c67ae6d406f85e1bd5368e2934e87c3d430e97adeb9dd9317c18d7c5d86a78e4b210fc87becd3e6f6171e951e6f7277b6e5833042697b8bbf12413462443e7ebca981f1b3957cd0031a6d91b7c2ba2962758a58c1ffbe7e46f4cad17506f9517bcc6841e927e1e1d1bb4f5aa1870aa29dedf1f81e43bec7e316f678acdfa6e5cf084ada3ddc179472deb9b2438b5e0484252b04e70c74689b3052cea1162e320e89b95648a4e1f380c9fc6c823325f7c68d9817943c6adbe5329c60d505bb5fd60272f41ed07be70b4a3887268c25ecec644ca2acda2451b09f7dd0d2e0f95d5deedeccb09307944f1fa2db07e91456af5f55569bcde6357b5757d5ee0d25eb8b5cfe3be716b48a65fef84301f9dd55f8eda59239e2905a54c2b1a5bb781a839f6488ada9069e7f77e347c3eab794247d5d8ce9c012ba94f4f20bf78b70d9c3baa1242d70b72881fd9a164a14f6ee97d3a03dc682bfff6ead6b8f47b07804916a125681c73079cb683b5919af737f957f0bbdfc8fb45b50cda5f802721509dc826c2186c98b84c68a486c79491233d7c2e53e068d11125a29f1e5e69b72dfa9af268f7a966db73270a684d42fe0a4b961f5e445d0ce523253a2db14a082758ad96f18f24c0c83d13279b1e07a9315ec6fdb0b3f1e84f9eb8f87dff38d6cbbe2a7f4f45505569b987bed3cc55550c1460917cdaa294135dbe4a0f458828ffb5e9611194a16524b58498db464cb1f56116f5fc82d1776f3ab2e72d976eca5c70238a4fbf7289d5531c24c896ca102c5d29c5d085a45926c20d3371c103c8e6ef2125791ac8817b0ce68d2345b38ba8ced1228ce2e0cf1f3b530374df8972ad286cef49f000000ffff72c22a543f060000", - "596e234fc3c0d58dd019eff083dc005f": "1f8b08000000000000ff8c545d6bdb30147db67ec59da0452aa9bd87b1878c3c94ac94b2252b74dbcb184595af55117d18e5a64d08feef438e937e6c747d926cdf73cebde71edc2abd5006a1569131ebdb9808042bb88e81704d9c15bcf1c4192bf8765bce627d95b0b1ebaedb6ecbb9f2d875950d84292857f958a3cb0863e96e755beae82b134f1749515c56c3d12e4ca595bec3caa3ef2f6f42c4d054ad72aab6e12df52e1ace24635565e2d860c0a40861570014a30383c16b469b16e1c66be887689446d8b2a2aac0eb319c2e703359e0e62c119ce6ca894162c534377d96c86a8742c3e05439dd9d23b07566fbf8418238e92d2987e211604a31c97f0be0bab509277559a38fe7fd032bceeafaad72235089e0b9a2048129bd22cb8acfe890f6fcbdd67f667a42d831d6ac8286393ecca64282f01a4ef66b2d67c36504ba815c2764efc080ce46dfab94d356e8c6c001378da1b126bf2518565e7eff36fbca0ac90adbf41493c3970b24c10f588ade7159fe085ea5e59d72e25893fcd443de4d205897558b84b44a8115dd133e4d3baaa9b318e82947d616c7ba31af12790d13781c628e0f3b0c2b740393617ed87a5d4e5d5ca2901ddbc3f7368a1a4e6a15255cd9606653a169fd7213cffccf2d1cfaaf4bafcb6ba48c1ac1f1a1914b42bffd829b31f0d606c347f053b9158ee1d7efdb0da1e06d0c86cb11f49153646318c3fbeeef595d34e579d615f9e7107aadabcbf985dc75238eee25eff72b7b3f5eccb68b9b78ccd192920d2633ef2aa1f1545eb7c9066a0457896e8e6a9e839763f6270000ffffb4a95a77a6040000", - "59b3f5316758be0d8bdfb8150516578a": "1f8b08000000000000ff5492b18ed4301086ebcc530c96901294d8fd4a5b2c9be69adb15f0023eaf63ac4d3c96336141e80a3a4a0a7a90780c5e0771af81622f5a5d15cde4ffbf19ff76d4e6ac9d451d3df8295262aca1128602db8f2ca012c3c402a012cef3fbe5411a9a94a3ee9c34d3acae9f78762a5856291a75d1e96443b110b9d14a47a30e4e5272caa5680434004ae12ec6bb1e25180a335fab2d8a7787feb0c15ddfe3ee78c4bb5e64f1bdbdec476f0363b0175c3168720dc312cced776d0687afca0eb2b4f61406ef5aa4c8334a2957b3ecbd1e0f913d8506ebde4e54b42dda942835f819aac2c7cd16afb867430a4f4ad940654cf6add262caf8fa1aa27cadcdd9255ac2a96e5a1c26966f63f281875a9cfc6ce8834d9f364a9deca09791d5cb59b4258ea681ca0f19fd628bc18feb5a55b2bca4b096792a548ff0bf776f2fb7b3d4c634ed2a83c71ce1d3f71f7fbf7e43f7e6b8c73fbf7f3dfdfc024a39da381b6cd26cb15c2632d188311193c1aecb5177ddc3b43e1199dbf02f0000ffff98f7b62e35020000", - "5ba5532a198294fc750ba2f01006d39b": "1f8b08000000000000ff4ccccf4ac0300c06f073fb14a587a120b3dd86caa02878f5e84d446a1b59b15d679afae7eda53a7497e4fb7e813cdcc6002b3d72e6cb2a8c909833cd6ddc90db4ef470d9ab5ef57ad6e3a82e4ecf5fd1522e4f1e52bea690205732ba7408d6dfffd70f0c0487be592c3fdd1056e86276e62e3b1b3bb7b40b994a2f57e9793a6b5b72661d857710460c8ab3e0638b7a8ffb5b61e4b448cede2ae0d79f093928958ae40c3ec11d78dc99d0ae079e7ef93b0000fffff662d40406010000", - "5f24168e1abae155c721ef6fa09bc429": "1f8b08000000000000ff8c914daedb201485c7b08a5b0f22a852584095512b75d2a4557f1640c83541b1015d885dc9f2de2b6ca7cd9b3cbd0142e25cbe730e24636fc621384a9673dfa748050467e90ccd34a963bc7c276cfd9f799e2675323dceb336c9379cb3c6f972bd9f958dbd76f1c38d4c89596f5bba396d636875329db9f8d0bc613e60d194ac1e0d5d30345c72ae359c7084802398252364a40149f1f61e6cd5441e2ca4b3fa8c7dfcb96812c498e1fd4a51ebd91e90a8ae481226ce0643b525b3ad8317839f6268bdab4a812dbafaf5edf89533c9996f17cce19ff2058b686a2c5562df3552fd0ebda17c359dd8d9223f2ee3ef0e107c575d1961b953e06c7e62d9b26256ff6746f5153bdbba57416386c3a3c309c715b35eabdfa87ea0f3b920fd7f2031e6adad907bc8839595b2dff254b1182a42f287cdccff060000ffff732d9b0b27020000", - "66133ffef1d9468dc3772e2c76e5a8d9": "1f8b08000000000000ff8c525fabda30147f4e3ec5596097e4d2a51f60f8e42e32984ea67b1e599ac4609a53d2e804f1bb8f34451c838b0fe5c0e9f9fd6d07a58fca19e81452eafb0153064e09d318b3b964462961cee7c3e9b7d4d8b70e3f1d93ca38b6f3188eaed54a1f4c9b4ce747f6cc3946db0e2aa8cec767ee033a4605a5f614356ccc9f1f45880be0095e2751396d1ad016ca0d170d9894ca8349c09592b34a2514d1d641452c315aefca2ac36c45aed54089a0c4db09beb8bf5899cc59c565ec0313f267ec551a0f2af0179dc5e7e9fec302a20f458e24934f2952727b20d3b9f22c8337313f72ecbfafbff1176dddbb4485a37ab857306128d1161673f06b92cb80a3e1a2202af83637c73b78ed140ad8fae82a81ce17983f7469a44c01fcdfeebc855fcd9ca193d5c1172cd006d8ee6dcf1a60838f6e9a181dfb3f454027df0a212fbf55943b93f9f6eb6625aa0cff78166c921035e9ecfb6f000000ffffd9fe30409b020000", - "67cc9f9db028b8b028595a334de05c39": "1f8b08000000000000ff9c544d8fdb36103d93bf62220486b4b049a42d7a50a143b36a8b005d77d1b4e780224732b114a95054ecc0d07f2f48c9bb7151a4467c10c9e1703ede7be341c827d12128e128d5fde07c809c924c3a1bf014324ab2a07bcc2825d9f9cc1e9c7af4d8ead33c9fcf6c2f7a9c67ae6d406f85e1bd5368e2934e87c3d430e97adeb9dd9317c18d7c5d86a78e4b210fc87becd3e6f6171e951e6f7277b6e5833042697b8bbf12413462443e7ebca981f1b3957cd0031a6d91b7c2ba2962758a58c1ffbe7e46f4cad17506f9517bcc6841e927e1e1d1bb4f5aa1870aa29dedf1f81e43bec7e316f678acdfa6e5cf084ada3ddc179472deb9b2438b5e0484252b04e70c74689b3052cea1162e320e89b95648a4e1f380c9fc6c823325f7c68d9817943c6adbe5329c60d505bb5fd60272f41ed07be70b4a3887268c25ecec644ca2acda2451b09f7dd0d2e0f95d5deedeccb09307944f1fa2db07e91456af5f55569bcde6357b5757d5ee0d25eb8b5cfe3be716b48a65fef84301f9dd55f8eda59239e2905a54c2b1a5bb781a839f6488ada9069e7f77e347c3eab794247d5d8ce9c012ba94f4f20bf78b70d9c3baa1242d70b72881fd9a164a14f6ee97d3a03dc682bfff6ead6b8f47b07804916a125681c73079cb683b5919af737f957f0bbdfc8fb45b50cda5f802721509dc826c2186c98b84c68a486c79491233d7c2e53e068d11125a29f1e5e69b72dfa9af268f7a966db73270a684d42fe0a4b961f5e445d0ce523253a2db14a082758ad96f18f24c0c83d13279b1e07a9315ec6fdb0b3f1e84f9eb8f87dff38d6cbbe2a7f4f45505569b987bed3cc55550c1460917cdaa294135dbe4a0f458828ffb5e9611194a16524b58498db464cb1f56116f5fc82d1776f3ab2e72d976eca5c70238a4fbf7289d5531c24c896ca102c5d29c5d085a45926c20d3371c103c8e6ef2125791ac8817b0ce68d2345b38ba8ced1228ce2e0cf1f3b530374df8972ad286cef49f000000ffff72c22a543f060000", - "69622533133ad8df518fe01a940713ec": "1f8b08000000000000ffe2525608c9c82c56c82c56485408f1f7f55148c94f2ecd4dcd2bd15370cacfcfade34a49cdcd77ad28c82c4a55b055503232c950e202040000ffffc36ed62935000000", - "70005e6633581fbd609bfec1957cd292": "1f8b08000000000000ffe2525608c9c82c56c82c56485408f1f7f55148c94f2ecd4dcd2bd15370cacfcfade34a49cdcd77ad28c82c4a55b055503232c950e202040000ffffc36ed62935000000", - "710a08c10a56dd632085fe2723c2fdca": "1f8b08000000000000ff8c90416bdc301085cf9a5f31ab931436f2bdc187a609edc2064a937b50e4b157d8968c24275bc2fef7328b5a96b69482f19899379fe7bdc5bad10e849d8d007e5e622aa840481743a1639120643fd9816bccfc2e948b0f83041072f0e5b0be1817e76688d763b225e6a696651c1a1743df2c76b29d0ff23ff495dd4cde1d246880579bb0c32b3e8e3f5d39628bf534736bdd38a4b8864e69807e0d0e9f289707eb839af1aac2cc83c677106cc23c5251ecac975b94c634c69c7f29f5e5fcb761d3453752ba76715e6226f3ddced3af85af3665521a44e7b37d9968efdd013fb418b3f94c85c2ab9277bbc78fb7fbfbe7fdeed317a971d3a294207c8f9bcb9d7710dca394789dfdf331eba2f4cdb9b96931f8e92c138b0dde294a49831027e087c36119a51413fc24b558b337bbe0cb5f5097a44a713d7292ecc9f7d86dd1f5db0a0bf4c6f9ded9a8f4cdbf49890afb98cdb73530caf515f887e967ac769fc8a62ebe9de52710319bfba32f2a51d170821f010000ffffa3cee358a7020000", - "75fe549e4c6ec53880d899e12f303d4d": "1f8b08000000000000ff64cdc14ec3300cc6f1337e0a1fe150274e18092fc13b646d3013cd3cb264429af6ee889603d54e3ef8f7d75774ea73c6eb95de52c9b71b802832b107a8f9ab1f6ac64740449443fbe87b1ab518d1e1b3a6a667b31e2ce9dc72bd67a2e654b5e9bebfe385c911ff9a878d99d35136ca935bd5f222ad62becd3137bc58b2640767f9d5beb8c871e79f77c394d88731c4c053f8eb54e64cff72a9a771d98fc4f004f0130000ffff1f9ac7b6f4000000", - "79906f2027d86895882c07b014541636": "1f8b08000000000000ff2a2d4e55c82e4a2cc92f8e4f49cdcdb7e6e2f2f40b760d0a51f0f40bf157482c2ac94cce492dd648c84c49d0514828c92cc9494dd0540873f409750d56d030d45150078ba96b5a7301020000ffffb60f97194b000000", - "7a430917a7aa07186808d95369458c4e": "1f8b08000000000000ff4ccc41ca02310c05e075738ad2030c497f7e14c1955770272eca248bc0743a74a25e5fa22ebacbfb1e2fb7cba2b2da1dc25aaac4734c2cb525085b6fd63cdbbc250885b97ba27c9870c2894e99329137b3e9d397ff084179f19310026b59ae5aa53dec3344ac7b82d0a5f0c0f9c7afae2683ff7d3dc618fde7d01c714ff00e0000ffff7dc54fc8b7000000", - "7b2c3c96383cfe733a24c5ae20b892e8": "1f8b08000000000000ff8a0e4e2d2a4b2d8ae552505050484c492952b0555032d003432b0b03030325b04c49666e6a7e690948d2b058890b100000ffffb7699cdc36000000", - "7b856fed41dcc4df47597de661eb9042": "1f8b08000000000000ff4ccc41cac2400c05e0f5e414610e5092fefc28822bafe04e5c0c4d16814ea74ca35e5fa22ebacbfb1e2fb7cb6cbaf81dd252aae219b3686d19d2da9bb7c83ead195211e991783c0c34d0c027e691399ac9ed19cb7f826432c7c90449accc57abda1efe1912d52d43ea5a64c7e38f5fdd5c77fef77544c4f8b96b8eb46578070000ffff6f600d93b7000000", - "7ba5a6706dc47731b76a9fccdc393d28": "1f8b08000000000000ff5cccc10ac2300cc6f173f214230f505a058782275fc19b78084b0e81761d5dd4d7970ac2d8f1ff83ef7bdcb2e9ec4f84998b0ed781444b2584a555afbd7d5a0881455aaf7418430c31a4cbe9389e09c124f7598a083cb9bdff21c6f96e45ebcb7fbb95109ab2ecedd3cc758ffd746b7125fc060000ffffe2756aadaa000000", - "7c8bcba5a2dfe4aa3bb18e6ee7c91140": "1f8b08000000000000ffbc91bd4a04311485ebe429428a416118333fa82c04052dc546bbc5229b5cd9c0fc7973e32ac3bcbb44071dabadb4ca395fc2e1836c6f5a0f3d3d71669c43a1852cab8b4215aa283775adce25672ef4894f3100ce9b6934211c0674f335d9f1e4f7ebd3b3c919323b1360be22dfc1104997214330eef1a71ed013acfa68307c764d18216b07abef066bdacceed30de948cf97ddaec9d329394b73b70ff7428bed11adeaffb4f2232af59fabe48b8f4cbf69c9bf82d0a2529c79d7a6582e71d9155a367bc9d94b047cff6642564a754172066f6057b85e30a1e957b8f9c21f010000ffff57d7944148020000", - "7d1ce2614230661551a2feabde69da0a": "1f8b08000000000000ff94924d4b1b411cc6effb29feecc504cc2eb6b7040fc50a164a1bac3d9512c6e49f71eaeece3833591a246063c1b6d8c616517c39d8628b976aa160b156fc30c926f15b94996455a81e7a49e6e5799edfcc33ebfb901c6e774ede5e7c3ee9ed1cc2bde203e86dbf0621b9e6d0dd58e99c1e43d23aea9c7e4dd656bb9bc7dd56ab7ffe03e8747102924fab30353353342ec7f707a6d95a75e84b5acdfed272deec400ee6b4162aeffb158c31e002a5f228e73440afcc43df5acb3cc8cdd6aa5594ca77543dd2e4258c836bf7eeba05c761a1e052834b999eabcd5a23e594fb29d8ceecc48e3c3b740b573e0bbc92632874fd361511cc2751c435d18c47ea52666f4acaf38422241fcf92b5f79db3f3defa012c122158a5e12dc62815e351c334d9dd3849feb44661b806c9d997e45b13e2b15188ef80e73969540543ee2994312ba3178f0d388f1ecf4ce6a1bbbb94ac7de81ffd4adeec5d6cedf7cfb73abf77ba9b7bbdf50343f87970b1f20e32ede66eaedddccdb65fedb797bf3b0e17e6dc407929458c834b04730be956e6b22e6ff85fa2a8354a552241908571a8924061c17186e782fb1872587400a42843914534933e62daa937693acd82445d939182db048561c813529fc220e019fb3b8d0bff6f7d3afdf026f7704989ac3d31c0d5ad6d2a11cc331fa5b9e740004051e7c1f5e725d15ce5cc93f88ad44b7326cab59a86a1370a4ec3714254cad49ab26d8ad292451422129abec7e0d9b592432e5113aa0c71a4ca6598778dce859804ac4234e65d890b3526b1e28e3cbf89a1c475c8048f3446fa1fce0bc5234da8c594079a41dcdf000000ffff1ddc747cee030000", - "8853489c373c278aef6338d0962fd746": "1f8b08000000000000ffe2525608c9c82c56c82c56485408f1f7f55148c94f2ecd4dcd2bd15370cacfcfade34a49cdcd77ad28c82c4a55b055503232c950e202040000ffffc36ed62935000000", - "91045d262e11302068a4d78ac8a2912e": "1f8b08000000000000ffa492516bdb3010c7dff3290ebde42572e384259b200f657d5cc9d614c6564250e55b7cd4d279d2c550d8871f72dd242bdda0ece5eefcf3dfe7fbeb04d0614cc4c1809a174b350248183b7298cc0800a0ba7fca00e4ed1e0df8c7f4b331ef8ac5805b8e929e351acaf97cba30390c0843479183c72027d5f5b7cd974fbb9bf5fa76f7f972b3f9babeb95a4566390a6ebfaf2e13d98b4d6dc3beb634bce8b839783cfb5d612e2a760f183506898f2d53104d81a4ba2faa41e5d87b1b2a03770300186bed6a1bad138c3aa1e86c1ae3ea203fde8f277fc8b869ac108773c9ee10c871853b47e341bc1d728db691dad5e81eccb18f60120377eae3f5959a80ea8fd0569e8282896a29ec33d5ba8d2cecb859896bd5f6f43579e48318984dd31152108c9d6d0c942718512261caca11c0e80955945eacb067afadaf5fe062befc60727883a3bea3760da9c1cff69541cfc73f7afaf7f41ebdb3aec6eae5257ce67f73312b676569ca1cdf60035dcd3927b19272f14b4d005470b92e67cb625a4c8bb27fc89dffdbe5ef000000ffff82fe973e7e030000", - "91350508efec72331fa05cfb386fe8d3": "1f8b08000000000000ff8c545d6bdb30147db67ec59da0452aa9bd87b1878c3c94ac94b2252b74dbcb184595af55117d18e5a64d08feef438e937e6c747d926cdf73cebde71edc2abd5006a1569131ebdb9808042bb88e81704d9c15bcf1c4192bf8765bce627d95b0b1ebaedb6ecbb9f2d875950d84292857f958a3cb0863e96e755beae82b134f1749515c56c3d12e4ca595bec3caa3ef2f6f42c4d054ad72aab6e12df52e1ace24635565e2d860c0a40861570014a30383c16b469b16e1c66be887689446d8b2a2aac0eb319c2e703359e0e62c119ce6ca894162c534377d96c86a8742c3e05439dd9d23b07566fbf8418238e92d2987e211604a31c97f0be0bab509277559a38fe7fd032bceeafaad72235089e0b9a2048129bd22cb8acfe890f6fcbdd67f667a42d831d6ac8286393ecca64282f01a4ef66b2d67c36504ba815c2764efc080ce46dfab94d356e8c6c001378da1b126bf2518565e7eff36fbca0ac90adbf41493c3970b24c10f588ade7159fe085ea5e59d72e25893fcd443de4d205897558b84b44a8115dd133e4d3baaa9b318e82947d616c7ba31af12790d13781c628e0f3b0c2b740393617ed87a5d4e5d5ca2901ddbc3f7368a1a4e6a15255cd9606653a169fd7213cffccf2d1cfaaf4bafcb6ba48c1ac1f1a1914b42bffd829b31f0d606c347f053b9158ee1d7efdb0da1e06d0c86cb11f49153646318c3fbeeef595d34e579d615f9e7107aadabcbf985dc75238eee25eff72b7b3f5eccb68b9b78ccd192920d2633ef2aa1f1545eb7c9066a0457896e8e6a9e839763f6270000ffffb4a95a77a6040000", - "991f8014a5f12bbf1f53265c7d62afcb": "1f8b08000000000000ff84914fcbdb3c10c4cfda4fb1afe12d7670a543e9c5c587149edefa5048a16745dec822b624e4b59362fcdd8b9cf4cfa1d08358586646bf61a336576d093b07e0c618126309a230c133ddb90051b01ba90010c5bacacfa1fb92e8e2eedbb6aef2558fb46dca79a6e4f5a0264a8b330fb175dccf6769c2a86c787b4d9ac3a49e235ead1a822d409c47fca7d213ab9e39aaf3a03b1af5c4940aa80094b2a1b1e42969267c38904318f0e612017f8f84c71871e2341bc615c4b4183c3c29e5e93141e4703c9c47f9e2adf3041bc065f6065fe9768cb1fc9ba9c6fe0f4785a58e110fc7186b344398e853f6e790b2aa9152ca2fa46a0591852dbe39c6b882c8400d4e8ba941ec180df635880dc4ef94f69993f985e17b8d467b4303362d3eaf24bf39eebfba91c2cce5cfdd476dae3685d97799e1ddfb43bea33c9109beab400877d9c19a167b79ea67eec2cd9786efd5877dff5f8bde0dfba7620856bee4026591214f69f9e578142bff5faa622f9a93b70cba3396d55e2611cfc9c3063f020000ffff936887516d020000", - "9a2d5bf0f7319185c80da50bc1e9bb25": "1f8b08000000000000ff9c90314bc3401cc5f77c8aff96045c0ae2521c62724a30b994f46ee8943bd3030f1b2bc9298e6eba281d8a1d74d04941280e226a07bf4c9bda6f21676b23bab9dc71efeefddebd7f9a0bae04b4b9e23bbc10b09773d52d92b6c8ba75e3f0b760b831720802e26c040818cf954c3ba2606019004cb619c87d65d56a3650dcf4b730f2004704300d02702889121fbb310a1126e046e1d76e8e5f46b3fed0f7cc15cd5052750483239ea7bb3cb7d656ed8ab0b44c7ae7d3bbc7b9215332130cf45a289e1d54cf3db4e9d080804be3186192103f444de2840d8830d086a78bfcbd5b8694d72793dec5f87d58f65fcbc1f36cf0340f4cff1158fdfcec6a327afb816bc47ee8c42dd8462db0f4046dadea1393c7c9a29cb568691bf6376add2c2f4fa70f371fb7f766ddf80c0000ffff632acdfbc6010000", - "9b5deb2555f0ec914474cefd5111dc33": "1f8b08000000000000ff9452c18adb30103d6bbe626a285890b5e935740fa5b4dd1c36a44d722e5a79ac0c912523c97417e37f2f52c2b2813db4175b9a79cfefcd1b8f4a9f95211c143b001e461f12d620aade2a5381a87cbc3cdbc8c6299b2ff1256a65cb31f1401580a8e6b979f4dd2e50cfcfcb32cfcd560db42c2dbb44c129db769cf186d3697a6ab41f5ae3efce41251fdbeb6b3c9b567bd7b7a3b2aa63f72f78eb4d0512a09f9c2e33d4126710d97cb35321522d41586f9a8de3543bb612db163b7a9a0c66d01a73b3e3703f8f2a9d16101df5144af5abf56ff9bdafabd7b93026155225415ccd5e042488df2bd499f87d727a851402aeefb1e3d2ff328e19c27da97fb847c736db15a372ac6b0a41825840e8cc19d4996a7d520e7d6cf625fc157e92202e8b68b63e71ff52eb155ef7d1ec373f1e8ebb9bfbcfe3e67053387cfbf57853d86c0f1244ef433112b3f2e73b0de2cdd486122abcc8e2c758ad3036fb14d8995aca4cfac3499f30962f6815e9bf1dac4108f11a5bce48bc9b3a3d73095d88fce3357b4b34d6972369efbad20a94a6e0deb1f270dc65a18e7a35d9b4bec12e39f805fe060000ffff29d8de170f030000", - "a9cbe2e1020ca7be3617cd6d83fe88f5": "1f8b08000000000000ff5256702c2dc9c82fe25256084a2dcb4c2d4f2de202040000ffffbe75c21514000000", - "ad071804417ba1d9b2afe916414fa2e0": "1f8b08000000000000ffa492516bdb3010c7dff3290ebde42572e384259b200f657d5cc9d614c6564250e55b7cd4d279d2c550d8871f72dd242bdda0ece5eefcf3dfe7fbeb04d0614cc4c1809a174b350248183b7298cc0800a0ba7fca00e4ed1e0df8c7f4b331ef8ac5805b8e929e351acaf97cba30390c0843479183c72027d5f5b7cd974fbb9bf5fa76f7f972b3f9babeb95a4566390a6ebfaf2e13d98b4d6dc3beb634bce8b839783cfb5d612e2a760f183506898f2d53104d81a4ba2faa41e5d87b1b2a03770300186bed6a1bad138c3aa1e86c1ae3ea203fde8f277fc8b869ac108773c9ee10c871853b47e341bc1d728db691dad5e81eccb18f60120377eae3f5959a80ea8fd0569e8282896a29ec33d5ba8d2cecb859896bd5f6f43579e48318984dd31152108c9d6d0c942718512261caca11c0e80955945eacb067afadaf5fe062befc60727883a3bea3760da9c1cff69541cfc73f7afaf7f41ebdb3aec6eae5257ce67f73312b676569ca1cdf60035dcd3927b19272f14b4d005470b92e67cb625a4c8bb27fc89dffdbe5ef000000ffff82fe973e7e030000", - "ae6a3278e56d79251f55ea74dcd489a8": "1f8b08000000000000ff94924f4b5b4d14c6f7f3290e7763027a2fbeef2ec145b18285d2066b57a584313919a7bdf7ce3833090d12b0b1605b6c638b28fe59d8628b9b6aa160b156fc30c94de2b72833c955a1bae8269999f33ce737e7991b04901c6e774ede5c7c3ae9ed1cc29dc23de86dbf02a98411d0dd58e99c1e43d23aea9c7e49d656bb9bc7dd56ab7ffe1dd84c6112928fab303d3b5bb02e120403d35cb532f425ad667f6939672b3006f3c6489d0b8232d630141295f699102c44bf24a2c0594b221c9bab562aa87440743d36f4054c80e76aff7b794278248532e0316ee6ab73cec80413410a763bb7712bdf2dbdfc95cf01afe4184953bf4d45250f681c0b430d17b1be94b94969e9396508c987b364ed5de7ecbcb77e008b544a5e6ef88b35549a8bb86193ec6e9c24bf5ba3303c83e4ec73f2b509b5f151a8fd07be4fd256658c84af51d57809fddaf880f3e0e1ec540ebabb4bc9dafbfed1cfe4f5dec5d67eff7cabf36ba7bbb9d75b3fb0841f07172b6f21d36eee8eb59bbbd9f6cbfdf6f2374284b4f706268a2962023c2ab9974f4b99cbb8fce17f91a131a8749186611626a042438d794286f782bb18095824004a96a0c06396491f31cdd49fb2996641a1a9aa58c36d82fcb0c9235a9fc6301419f73b830bff6e7d3c73ff26f7f048cbacbb31c0d5d4ae2b95dcb71fa59d732000606872e005f62d024debc579dbc373c586c536f2a44148845adb3c53a8b36ba378cc20a6910d7a1c9e5c4b37120a0d65daa2462a424539cfea3ca8d19097a9c19ca770a1ca1596bd91a73731b4bc0e9914b1c1d8fcc579a6456c287398d2403368f7270000ffff07b2c244e7030000", - "b264df098d6769dab4f670828910fa60": "1f8b08000000000000ff52567049cdcde7e252565678b970e7f3d9eb9eaf6b78b2bb9bcb508f0b100000ffff24c85b041b000000", - "b2ab5cb21b5a72490b36cbd46cdfc102": "1f8b08000000000000ff52567049cdcde7e252565678b970e7f3d9eb9eaf6b78b2bb9bcb508f0b100000ffff24c85b041b000000", - "bac80cf86d8252f4a488b221da8cfe80": "1f8b08000000000000ffbc91bd4a04311485ebe429428a416118333fa82c04052dc546bbc5229b5cd9c0fc7973e32ac3bcbb44071dabadb4ca395fc2e1836c6f5a0f3d3d71669c43a1852cab8b4215aa283775adce25672ef4894f3100ce9b6934211c0674f335d9f1e4f7ebd3b3c919323b1360be22dfc1104997214330eef1a71ed013acfa68307c764d18216b07abef066bdacceed30de948cf97ddaec9d329394b73b70ff7428bed11adeaffb4f2232af59fabe48b8f4cbf69c9bf82d0a2529c79d7a6582e71d9155a367bc9d94b047cff6642564a754172066f6057b85e30a1e957b8f9c21f010000ffff57d7944148020000", - "bb9c567981605c082029d83839523a6c": "1f8b08000000000000ff94924d4b1b411cc6effb29feecc504cc2eb6b7040fc50a164a1bac3d9512c6e49f71eaeece3833591a246063c1b6d8c616517c39d8628b976aa160b156fc30c926f15b94996455a81e7a49e6e5799edfcc33ebfb901c6e774ede5e7c3ee9ed1cc2bde203e86dbf0621b9e6d0dd58e99c1e43d23aea9c7e4dd656bb9bc7dd56ab7ffe03e8747102924fab30353353342ec7f707a6d95a75e84b5acdfed272deec400ee6b4162aeffb158c31e002a5f228e73440afcc43df5acb3cc8cdd6aa5594ca77543dd2e4258c836bf7eeba05c761a1e052834b999eabcd5a23e594fb29d8ceecc48e3c3b740b573e0bbc92632874fd361511cc2751c435d18c47ea52666f4acaf38422241fcf92b5f79db3f3defa012c122158a5e12dc62815e351c334d9dd3849feb44661b806c9d997e45b13e2b15188ef80e73969540543ee2994312ba3178f0d388f1ecf4ce6a1bbbb94ac7de81ffd4adeec5d6cedf7cfb73abf77ba9b7bbdf50343f87970b1f20e32ede66eaedddccdb65fedb797bf3b0e17e6dc407929458c834b04730be956e6b22e6ff85fa2a8354a552241908571a8924061c17186e782fb1872587400a42843914534933e62daa937693acd82445d939182db048561c813529fc220e019fb3b8d0bff6f7d3afdf026f7704989ac3d31c0d5ad6d2a11cc331fa5b9e740004051e7c1f5e725d15ce5cc93f88ad44b7326cab59a86a1370a4ec3714254cad49ab26d8ad292451422129abec7e0d9b592432e5113aa0c71a4ca6598778dce859804ac4234e65d890b3526b1e28e3cbf89a1c475c8048f3446fa1fce0bc5234da8c594079a41dcdf000000ffff1ddc747cee030000", - "bcd84391b0e45f9d8641de9b0f6bdc98": "1f8b08000000000000ff2a2d4e55c82e4a2cc92f8e4f49cdcdb7e6e2f2f40b760d0a51f0f40bf157482c2ac94cce492dd648c84c49d0514828c92cc9494dd0540873f409750d56d030d45150078ba96b5a7301020000ffffb60f97194b000000", - "c00dc165bbf20a017bf129c26a1f8e7d": "1f8b08000000000000ff8a0e4e2d2a4b2d8ae552505050484c492952b0555032d003432b4b03030325b04c49666e6a7e690948d2b058890b100000ffffb9f9177936000000", - "c1d37027fe90bb3a8ed6d3857637d326": "1f8b08000000000000ff8c534d6edb3c105d8ba71813f802ea834bed5378d126818bb6f9419ca0cb82924734618a54a99163c0f0317a83163d40d73d4e7a8e8294edba08fab3304c7186efcd7b7c6c55b5541a6141d432669ad60702c132ee908ab8c919cbda12f866232ffdfc26606dd6dbed6623af5483db6da15ac359f6dbb27184c1295b347e8e36766a438bbe94956f0aed9f2d8322df15bbbf76a98bcabbba68955573e3fea5df7acd595636f0d7cebda4a2b46a8e8dea08036739632b15a05b55d096f21c1b3fc3b0c2c05851c0153e80c307505036d0a57dc9eade55b122ba5f4fe420d069e310fe2f1b79919663c010e2cf871c362c8b54826559556b281b391c3cf3ae363aee12ec94cbbbebcbb72ccb5966ea04313954a64882471d927c63792eef5da342b750569c54943f4feda309386323631690fae058b63dc2aa688019f88f3122af38a96afd47a0e8d6043a96ed044fa29873ac556f69c01c306276e42d6a13bd8e4ebdbcdc9571674f17253a43b7bea7c376ceb261cee153ce48051239db8fb065c3251c1f3c323d398df2c6382d5ae3e2181a4e2780721a7cdf0a5e1c02ca7396456d5a4e2fee042fbac8c4c7b0f00f773ed1e651f09e30a2898ad689eccc3bc235253653c3fbe1ae4f27314b037945eb71b4efa997d66b7911532178c41c1222fe5be53ca1e4290c6bf9a2f481de195acc4851df8974edc33afa682abc776aa58c55a5c5fda44501b8564d6b116a1fd2d386801f7aec0816cacded21c43f558aea89a465947292dead7c935e511cfc155aeb4f814fbd554ec3e3a72fdfbf7e7cfcfc0d46a3111fa77054f2f5ecfa4a2c07e96cfb230000ffffb57309ca64040000", - "c5dc8e7e6833a138a5738b8d0eaa4040": "1f8b08000000000000ff9c90314bc3401cc5f77c8aff96045c0ae2521c62724a30b994f46ee8943bd3030f1b2bc9298e6eba281d8a1d74d04941280e226a07bf4c9bda6f21676b23bab9dc71efeefddebd7f9a0bae04b4b9e23bbc10b09773d52d92b6c8ba75e3f0b760b831720802e26c040818cf954c3ba2606019004cb619c87d65d56a3650dcf4b730f2004704300d02702889121fbb310a1126e046e1d76e8e5f46b3fed0f7cc15cd5052750483239ea7bb3cb7d656ed8ab0b44c7ae7d3bbc7b9215332130cf45a289e1d54cf3db4e9d080804be3186192103f444de2840d8830d086a78bfcbd5b8694d72793dec5f87d58f65fcbc1f36cf0340f4cff1158fdfcec6a327afb816bc47ee8c42dd8462db0f4046dadea1393c7c9a29cb568691bf6376add2c2f4fa70f371fb7f766ddf80c0000ffff632acdfbc6010000", - "c84c3b26a959752d7eb926d02ce46ddd": "1f8b08000000000000ff9c544d8fdb36103d93bf62220486b4b049a42d7a50a143b36a8b005d77d1b4e780224732b114a95054ecc0d07f2f48c9bb7151a4467c10c9e1703ede7be341c827d12128e128d5fde07c809c924c3a1bf014324ab2a07bcc2825d9f9cc1e9c7af4d8ead33c9fcf6c2f7a9c67ae6d406f85e1bd5368e2934e87c3d430e97adeb9dd9317c18d7c5d86a78e4b210fc87becd3e6f6171e951e6f7277b6e5833042697b8bbf12413462443e7ebca981f1b3957cd0031a6d91b7c2ba2962758a58c1ffbe7e46f4cad17506f9517bcc6841e927e1e1d1bb4f5aa1870aa29dedf1f81e43bec7e316f678acdfa6e5cf084ada3ddc179472deb9b2438b5e0484252b04e70c74689b3052cea1162e320e89b95648a4e1f380c9fc6c823325f7c68d9817943c6adbe5329c60d505bb5fd60272f41ed07be70b4a3887268c25ecec644ca2acda2451b09f7dd0d2e0f95d5deedeccb09307944f1fa2db07e91456af5f55569bcde6357b5757d5ee0d25eb8b5cfe3be716b48a65fef84301f9dd55f8eda59239e2905a54c2b1a5bb781a839f6488ada9069e7f77e347c3eab794247d5d8ce9c012ba94f4f20bf78b70d9c3baa1242d70b72881fd9a164a14f6ee97d3a03dc682bfff6ead6b8f47b07804916a125681c73079cb683b5919af737f957f0bbdfc8fb45b50cda5f802721509dc826c2186c98b84c68a486c79491233d7c2e53e068d11125a29f1e5e69b72dfa9af268f7a966db73270a684d42fe0a4b961f5e445d0ce523253a2db14a082758ad96f18f24c0c83d13279b1e07a9315ec6fdb0b3f1e84f9eb8f87dff38d6cbbe2a7f4f45505569b987bed3cc55550c1460917cdaa294135dbe4a0f458828ffb5e9611194a16524b58498db464cb1f56116f5fc82d1776f3ab2e72d976eca5c70238a4fbf7289d5531c24c896ca102c5d29c5d085a45926c20d3371c103c8e6ef2125791ac8817b0ce68d2345b38ba8ced1228ce2e0cf1f3b530374df8972ad286cef49f000000ffff72c22a543f060000", - "ca7acd4efcb673fe4e3505be171752b7": "1f8b08000000000000ff8c9141ebd43010c5cf994f31169476a9ed41bc547a58416f2ec20a9e633a9b0ddb26613aedae947e7749bb2a8887ff210c0cefbdfc1e13b5b9694bd8390037c4c08239a8cc042ff4900c54266ea00c4065cb527d09dd57a68b7bacebb254273dd0bad6ce0bb1d77d3d12cfceec62ebe43afda84c186a1bdede584b18ebe788375bf7c1662f9079929aa3a9ef9a3bf2191400756d4363c9136b21dcb52821f478774c203f23e131461c852723b8801a678387275c75de2728cbd1e0610fdeb6c4b0025c266ff044f7638cf9ff8c25da7f5c05e63a463c1c632cd1f461a4cf292305e54589c49c5ee0620195842dbe39c6b8804a600d8eb329416d380dda12d40aea6f4afbcc493d94914789467b433d362d3e8f547d7772fde6060a93e4bf771fb5b9590e93ef12c3bbf78774c6ea4c26f8ae00a5dc65036b5ab4d5f93a4917ee3e37f2283e6cfb572d7ad76f9faa3ed8ea532a906709f2ccf31fc75e2c7f3d17d9563425af097463cc8bad0c934cec61855f010000ffffbbe7f8956c020000", - "d11e25ce5264ce1ed0f0863d28fa7113": "1f8b08000000000000ff8c525fabda30147f4e3ec5596097e4d2a51f60f8e42e32984ea67b1e599ac4609a53d2e804f1bb8f34451c838b0fe5c0e9f9fd6d07a58fca19e81452eafb0153064e09d318b3b964462961cee7c3e9b7d4d8b70e3f1d93ca38b6f3188eaed54a1f4c9b4ce747f6cc3946db0e2aa8cec767ee033a4605a5f614356ccc9f1f45880be0095e2751396d1ad016ca0d170d9894ca8349c09592b34a2514d1d641452c315aefca2ac36c45aed54089a0c4db09beb8bf5899cc59c565ec0313f267ec551a0f2af0179dc5e7e9fec302a20f458e24934f2952727b20d3b9f22c8337313f72ecbfafbff1176dddbb4485a37ab857306128d1161673f06b92cb80a3e1a2202af83637c73b78ed140ad8fae82a81ce17983f7469a44c01fcdfeebc855fcd9ca193d5c1172cd006d8ee6dcf1a60838f6e9a181dfb3f454027df0a212fbf55943b93f9f6eb6625aa0cff78166c921035e9ecfb6f000000ffffd9fe30409b020000", - "d27273567e8a0cdf9c0f35717b89ff8d": "1f8b08000000000000ff248eb16eeb300c45e7f02b2e3249ef05d6de31f5da0e457e80b61959b52d1a120d0f45ffbdb03391e0bd073c21e07fb7a579c09e8aa4fc2dbd5108788c82d7dd3862e1492aea5604360aaa6d1d5245563b4b8694cfe09932cf2fae215ab99f380a0656a2b4ac5a0c8e2ed7986cdcbaa6d72544d5384b387e5fc9138510f52d4a96c226980a9b5698ea7cead173cb3db2ec0fa9d6b23a0ff76f60bde1089cbf414ad1e2f143979573eadd4135f7c3c765d9dba3fa297b7b3fc7970ca99edbc7bbf7f44b7f010000ffff3c96d9e80d010000", - "d391eec375a18e4524decc911a55cb37": "1f8b08000000000000ff64cdc14ec3300cc6f1337e0a1fe150274e18092fc13b646d3013cd3cb264429af6ee889603d54e3ef8f7d75774ea73c6eb95de52c9b71b802832b107a8f9ab1f6ac64740449443fbe87b1ab518d1e1b3a6a667b31e2ce9dc72bd67a2e654b5e9bebfe385c911ff9a878d99d35136ca935bd5f222ad62becd3137bc58b2640767f9d5beb8c871e79f77c394d88731c4c053f8eb54e64cff72a9a771d98fc4f004f0130000ffff1f9ac7b6f4000000", - "d48e95472ef22ab5c6e49c918ff4c7f7": "1f8b08000000000000ff5cccc10ac2300cc6f173f214230f505a058782275fc19b78084b0e81761d5dd4d7970ac2d8f1ff83ef7bdcb2e9ec4f84998b0ed781444b2584a555afbd7d5a0881455aaf7418430c31a44b3a1dc7332198e4be4b118127b7f73fc438dfad687df96fb812425396bd7d9ab9eeb19f6e2dae84df000000ffffd60a1b01ab000000", - "d6bf2490901d7994259b59036e5ee787": "1f8b08000000000000ff8a0e4e2d2a4b2d8ae552505050484c492952b0555032d003432b4b03030325b04c49666e6a7e690948d2b058890b100000ffffb9f9177936000000", - "d9e24a8fbdaa656b04d082a11a16fab2": "1f8b08000000000000ff8c525fabda30147f4e3ec5596097e4d2a51f60f8e42e32984ea67b1e599ac4609a53d2e804f1bb8f34451c838b0fe5c0e9f9fd6d07a58fca19e81452eafb0153064e09d318b3b964462961cee7c3e9b7d4d8b70e3f1d93ca38b6f3188eaed54a1f4c9b4ce747f6cc3946db0e2aa8cec767ee033a4605a5f614356ccc9f1f45880be0095e2751396d1ad016ca0d170d9894ca8349c09592b34a2514d1d641452c315aefca2ac36c45aed54089a0c4db09beb8bf5899cc59c565ec0313f267ec551a0f2af0179dc5e7e9fec302a20f458e24934f2952727b20d3b9f22c8337313f72ecbfafbff1176dddbb4485a37ab857306128d1161673f06b92cb80a3e1a2202af83637c73b78ed140ad8fae82a81ce17983f7469a44c01fcdfeebc855fcd9ca193d5c1172cd006d8ee6dcf1a60838f6e9a181dfb3f454027df0a212fbf55943b93f9f6eb6625aa0cff78166c921035e9ecfb6f000000ffffd9fe30409b020000", - "dc9ddbcd9cdbb28fa4632260727f292c": "1f8b08000000000000ff248eb16eeb300c45e7f02b2e3249ef05d6de31f5da0e457e80b61959b52d1a120d0f45ffbdb03391e0bd073c21e07fb7a579c09e8aa4fc2dbd5108788c82d7dd3862e1492aea5604360aaa6d1d5245563b4b8694cfe09932cf2fae215ab99f380a0656a2b4ac5a0c8e2ed7986cdcbaa6d72544d5384b387e5fc9138510f52d4a96c226980a9b5698ea7cead173cb3db2ec0fa9d6b23a0ff76f60bde1089cbf414ad1e2f143979573eadd4135f7c3c765d9dba3fa297b7b3fc7970ca99edbc7bbf7f44b7f010000ffff3c96d9e80d010000", - "ddd2d8d002a9cac8b4c670cb20f96f3c": "1f8b08000000000000ff52567049cdcde7e252565678b970e7f3d9eb9eaf6b78b2bb9bcb508f0b100000ffff24c85b041b000000", - "deae3975927265bbe2e86537858be5d7": "1f8b08000000000000ff8c51cb8e1331103cdb5fd1cc616547c1be202e2807362b7121e1fd013d7ead158f9d783a246834ff8e1c061471401cac96dc5d55dd554734070c0e2c16cee3702c954070d69992c95da9e39c75d3a476c57eaccec7eb3c4f93dae3e0e659c74cae664c7a28d6a58eb32e447a3ef7ca944187f2f25091caa897723c046d4af6fa88096dccff336f91b0c7d1e9f1943a2e39f7e76c60ef2e4f8f4282b03dacc653524f8f6b301e5a53c835b85adb2b55c2c4d977aced22667c8036bc2dd9c7d03e089655d4d70fbbf79c49cea2bfa1377f3aef1c89cef68aca903aa9bee501ebf88c493c18926f6ec32f3690636a52ac3a3ad7ccd97cc764e817c9364597e99ea3a98a07e3c33f896c0f9bdbe27b77d9fdf8f2698170663c6c969b61b2bddaa6323a2167fe1b3d2f7e090b2b8b45c267bcbcad144d72c2d015968c9b23adae215a88995ebf9220b012ac6eb9aa05f2b7af5a4370042d22f0b50c60fb3be59f010000ffff90ef4f8158020000", - "e141e2657bd9e59d6741ab94b976c8d9": "1f8b08000000000000ff4ccccf4ac0300c06f073fb14a587a120b3dd86caa02878f5e84d446a1b59b15d679afae7eda53a7497e4fb7e813cdcc6002b3d72e6cb2a8c909833cd6ddc90db4ef470d9ab5ef57ad6e3a82e4ecf5fd1522e4f1e52bea690205732ba7408d6dfffd70f0c0487be592c3fdd1056e86276e62e3b1b3bb7b40b994a2f57e9793a6b5b72661d857710460c8ab3e0638b7a8ffb5b61e4b448cede2ae0d79f093928958ae40c3ec11d78dc99d0ae079e7ef93b0000fffff662d40406010000", - "e14aca05bd4c126f7220c63941266e17": "1f8b08000000000000ff5256567049cdcde7e2525656562833d433d033e032d45378b2a3ebf9aefd4fbbe6bf68de5b5151c105080000ffff79fbc41327000000", - "e2903d19bafc045ba9a6890ea9a43a52": "1f8b08000000000000ffe2525608c9c82c56c82c56485408f1f7f55148c94f2ecd4dcd2bd15370cacfcfade34a49cdcd77ad28c82c4a55b055503232c950e202040000ffffc36ed62935000000", - "e37cb3fefed2bf7aa681a848a63af049": "1f8b08000000000000ff4ccc41cac2400c05e0f5e414610e5092fefc28822bafe04e5c0c4d16814ea74ca35e5fa22ebacbfb1e2fb7cb6cbaf81dd252aae219b3686d19d2da9bb7c83ead195211e991783c0c34d0c027e691399ac9ed19cb7f826432c7c90449accc57abda1efe1912d52d43ea5a64c7e38f5fdd5c77fef77544c4f8b96b8eb46578070000ffff6f600d93b7000000", - "e4ce7202d112d82a5e014831a14d1ba5": "1f8b08000000000000ff2a484cce4e4c4f55c8cd4f49cde1e2d2d757f02e4a2cc92f56c848cdc9c957c80673f4b84a2a0b526132c52545a5c9250ad55c9c1e6035c525459979e95cb55c10558e452599c939a948ca3c5d1432f34acc4cb8389df3f34a52f34a605a381d4b4b32f28be02600020000ffffcb8696638d000000", - "e5e2572a852f6474077b82fb7793df25": "1f8b08000000000000ff8c534d6edb3c105d8ba71813f802ea834bed5378d126818bb6f9419ca0cb82924734618a54a99163c0f0317a83163d40d73d4e7a8e8294edba08fab3304c7186efcd7b7c6c55b5541a6141d432669ad60702c132ee908ab8c919cbda12f866232ffdfc26606dd6dbed6623af5483db6da15ac359f6dbb27184c1295b347e8e36766a438bbe94956f0aed9f2d8322df15bbbf76a98bcabbba68955573e3fea5df7acd595636f0d7cebda4a2b46a8e8dea08036739632b15a05b55d096f21c1b3fc3b0c2c05851c0153e80c307505036d0a57dc9eade55b122ba5f4fe420d069e310fe2f1b79919663c010e2cf871c362c8b54826559556b281b391c3cf3ae363aee12ec94cbbbebcbb72ccb5966ea04313954a64882471d927c63792eef5da342b750569c54943f4feda309386323631690fae058b63dc2aa688019f88f3122af38a96afd47a0e8d6043a96ed044fa29873ac556f69c01c306276e42d6a13bd8e4ebdbcdc9571674f17253a43b7bea7c376ceb261cee153ce48051239db8fb065c3251c1f3c323d398df2c6382d5ae3e2181a4e2780721a7cdf0a5e1c02ca7396456d5a4e2fee042fbac8c4c7b0f00f773ed1e651f09e30a2898ad689eccc3bc235253653c3fbe1ae4f27314b037945eb71b4efa997d66b7911532178c41c1222fe5be53ca1e4290c6bf9a2f481de195acc4851df8974edc33afa682abc776aa58c55a5c5fda44501b8564d6b116a1fd2d386801f7aec0816cacded21c43f558aea89a465947292dead7c935e511cfc155aeb4f814fbd554ec3e3a72fdfbf7e7cfcfc0d46a3111fa77054f2f5ecfa4a2c07e96cfb230000ffffb57309ca64040000", - "ece08322e1d8a72e1c014ac64ace18a9": "1f8b08000000000000ff94924f8bdb3010c5cf9a4f3135b4d8c1b50fa517971c52d8deba1452e859916765115b12e3719262fcdd8becf42f856e0f466678f39bf78689da9cb5256c1d801b6260c11c54668217ba49062a13375006a0b279ae3e86f613d393bb2dcb3c578f7aa065a99d1762affb7a24be38b389ad936e3a55260cb50dafcfac258cf5fd89675bf7c166a04e03fe53e949ea4e24d6a75eb734e85188b3674c487d1c4d7dd5dc92cfa000a86b1b1a4b9e580be1a64509a1c7ab6302f91a090f31e2283c19c119d47831b8bbe7aa8edb0b2ad9c1dd69a81ebc759e40598e0677dba455460c0bc0d3e40d3ed2f51063fe375289dd2f9812ed1f8c02731d23ee0e319668fa30d287444cd8bc289198d317b8984125e11e5f1d629c4125df0d8e1753825add36d8a5dfe4b3415b825a40fd04eeefc8945819b99568b437d463b3c7fb25545f9c749fdd406192fc7bedbd3667cb61f26db2f3e6ed2edd4a7524137c5b8052ee69f5d8ecd156c76e92365c7d6ee456bc5beb2ff6e85dbf0e557db0d543ca9267c9e4912f3f3ab68cf9cb4b91ad991379f98ddefd073d6de339f46d0379b1ae8a4926f6b0c0b7000000fffffef7822e2f030000", - "ef3624e258287c266b0982f7c486d27b": "1f8b08000000000000ff94924d6fdb300c86cfd2af200c2c90324fbe17c865ed801dda2c6856ecacc8b423d49654594e1a18feef83fcd1a6cbba8f8325987a45be8f4827d5a32c111af407ad90525d3beb03304a12654dc0e790509214754828256e0749d7893b9b6f3c16fab9efbb4eac658d7d9f49a7a3f2bd636d027a23ab2c9736ea4a1df6ed4e285b67a5fdf4e865b04d366deeb1cc943545e66425736d62e9b7172a69cacc791becae2d32174e0e9b0c6b174e17b96d596176d41e13ca293d480f1b6f0f3a470f2b8871b1c6e316035be3311d039fb5c999c123733b7183b5dda23fa0e729c4d8723b3e15e79cd22c83e9777e4141a399d768f0ad0ad0512215c072021277d251924b0bb9b4e2465ada0fb9d6788c3540ced9409a1c3c86d61b418bd6a82861f97c8d036b603694822a206a184f01bd8f9ff53cd66e60058b49d65142a4ba0258cc66be7fbbbbedfa9492e8e80af294929e1255c00a1a715dd9062989f95630dff82183dab3443a57692583b646045b57490a8d908a53325a9ea8b6f2f415abca42e99d821c6b3bd89c80ce08f88b94a9f00cd3fc89eb714fc1e3132cdd4e0c927b7ce2c03cbaea04cba1f5e24b5c7f651f15aba17767324e495107b1f1da848225fbc1e0872619aa0c43fb1ec6c3fd2decea7fe378b8bffd5f94d760e3de63599c8b624387bc265cc10492c0c7178eb19d97b0adaffe0cbcd1a6041797b047f0d8d8d6c701bfc48dcadf73e29bde706017ad7a418b95617176dcf5719ee2a8cff9f9e46c184a50c3fa376f8396c51a3dfd190000ffff35636a08ee040000", - "f00e9b617b63f2dc20a592cf7aeb0f8a": "1f8b08000000000000ff8c90316ec3300c45e7f0148427bb35a4bd5bba756890a11750644661638b024d27050cdfbdb0d3a163d6c7f781cfef3dbe9e26ee3bbcb312e76f8a06dee3d785f0c12d241cc295461c2725b40be168d30979c42cb649869cb7c39973e81f39075042bc8644d831000f45d4b0865d35cfee53baa3d2997f96659edd210cb42c9eb391e6d0fb2e48f58c3792de38d2d32ea94f5a6205b0ab12db653ab928834f22a927bffe5f4103e07d92b744993418e15583c988b6ed03e72947fcc86cfb52ea06eb977d292daeb46e5a2455d10667d8959039d66bc4bdaf6bd45d107754b97147dae25ff37f642de60e746ff140f77d294d030bfc060000ffffe672418b9d010000", - "f02c908885e29d26bfb4de9c33c5e4d1": "1f8b08000000000000ff8c545d6bdb30147db67ec59da0452aa9bd87b1878c3c94ac94b2252b74dbcb184595af55117d18e5a64d08feef438e937e6c747d926cdf73cebde71edc2abd5006a1569131ebdb9808042bb88e81704d9c15bcf1c4192bf8765bce627d95b0b1ebaedb6ecbb9f2d875950d84292857f958a3cb0863e96e755beae82b134f1749515c56c3d12e4ca595bec3caa3ef2f6f42c4d054ad72aab6e12df52e1ace24635565e2d860c0a40861570014a30383c16b469b16e1c66be887689446d8b2a2aac0eb319c2e703359e0e62c119ce6ca894162c534377d96c86a8742c3e05439dd9d23b07566fbf8418238e92d2987e211604a31c97f0be0bab509277559a38fe7fd032bceeafaad72235089e0b9a2048129bd22cb8acfe890f6fcbdd67f667a42d831d6ac8286393ecca64282f01a4ef66b2d67c36504ba815c2764efc080ce46dfab94d356e8c6c001378da1b126bf2518565e7eff36fbca0ac90adbf41493c3970b24c10f588ade7159fe085ea5e59d72e25893fcd443de4d205897558b84b44a8115dd133e4d3baaa9b318e82947d616c7ba31af12790d13781c628e0f3b0c2b740393617ed87a5d4e5d5ca2901ddbc3f7368a1a4e6a15255cd9606653a169fd7213cffccf2d1cfaaf4bafcb6ba48c1ac1f1a1914b42bffd829b31f0d606c347f053b9158ee1d7efdb0da1e06d0c86cb11f49153646318c3fbeeef595d34e579d615f9e7107aadabcbf985dc75238eee25eff72b7b3f5eccb68b9b78ccd192920d2633ef2aa1f1545eb7c9066a0457896e8e6a9e839763f6270000ffffb4a95a77a6040000", - "f33c13a2bed49bfb098a354f3fb3d263": "1f8b08000000000000ff4ccccf4ac0300c06f073fb14a587a120b3dd86caa02878f5e84d446a1b59b15d679afae7eda53a7497e4fb7e813cdcc6002b3d72e6cb2a8c909833cd6ddc90db4ef470d9ab5ef57ad6e3a82e4ecf5fd1522e4f1e52bea690205732ba7408d6dfffd70f0c0487be592c3fdd1056e86276e62e3b1b3bb7b40b994a2f57e9793a6b5b72661d857710460c8ab3e0638b7a8ffb5b61e4b448cede2ae0d79f093928958ae40c3ec11d78dc99d0ae079e7ef93b0000fffff662d40406010000", - "f44350500991f17cec6420d8300b4f3d": "1f8b08000000000000ff5cccc10ac2300cc6f173f214230f505a058782275fc19b78084b0e81761d5dd4d7970ac2d8f1ff83ef7bdcb2e9ec4f84998b0ed781444b2584a555afbd7d5a0881455aaf7418430c31a44b3a1dc7332198e4be4b118127b7f73fc438dfad687df96fb812425396bd7d9ab9eeb19f6e2dae84df000000ffffd60a1b01ab000000", - "f5322c592d97a1cdc37376bd6c2bda44": "1f8b08000000000000ff2a2d4e55c82e4a2cc92f8e4f49cdcdb7e6e2f2f40b760d0a51f0f40bf157482c2ac94cce492dd648c84c49d0514828c92cc9494dd0540873f409750d56d030d45150078ba96b5a7301020000ffffb60f97194b000000", - "f8007e4822e340ca304d913fcd9d783b": "1f8b08000000000000ff8c914daedb201485c7b08a5b0f22a852584095512b75d2a4557f1640c83541b1015d885dc9f2de2b6ca7cd9b3cbd0142e25cbe730e24636fc621384a9673dfa748050467e90ccd34a963bc7c276cfd9f799e2675323dceb336c9379cb3c6f972bd9f958dbd76f1c38d4c89596f5bba396d636875329db9f8d0bc613e60d194ac1e0d5d30345c72ae359c7084802398252364a40149f1f61e6cd5441e2ca4b3fa8c7dfcb96812c498e1fd4a51ebd91e90a8ae481226ce0643b525b3ad8317839f6268bdab4a812dbafaf5edf89533c9996f17cce19ff2058b686a2c5562df3552fd0ebda17c359dd8d9223f2ee3ef0e107c575d1961b953e06c7e62d9b26256ff6746f5153bdbba57416386c3a3c309c715b35eabdfa87ea0f3b920fd7f2031e6adad907bc8839595b2dff254b1182a42f287cdccff060000ffff732d9b0b27020000", - "fd07d10d1e374aa9cdc1a9b1189e2f05": "1f8b08000000000000ff5cccc10ac2300cc6f173f214230f505a058782275fc19b78084b0e81761d5dd4d7970ac2d8f1ff83ef7bdcb2e9ec4f84998b0ed781444b2584a555afbd7d5a0881455aaf7418430c31a4cbe9389e09c124f7598a083cb9bdff21c6f96e45ebcb7fbb95109ab2ecedd3cc758ffd746b7125fc060000ffffe2756aadaa000000", - "fda1b2ac9058723473a3ae92bef45575": "1f8b08000000000000ff5cccc10ac2300cc6f173f214230f505a058782275fc19b78084b0e81761d5dd4d7970ac2d8f1ff83ef7bdcb2e9ec4f84998b0ed781444b2584a555afbd7d5a0881455aaf7418430c31a44b3a1dc7332198e4be4b118127b7f73fc438dfad687df96fb812425396bd7d9ab9eeb19f6e2dae84df000000ffffd60a1b01ab000000", - }) - if err != nil { - panic(err) - } - g.DefaultResolver = hgr - - func() { - b := packr.New("all", "./templates/all") - b.SetResolver("CHANGELOG.md", packr.Pointer{ForwardBox: gk, ForwardPath: "4bafcbfe7d0faa99ecff3bd27c22d83f"}) - b.SetResolver("OWNERS", packr.Pointer{ForwardBox: gk, ForwardPath: "09569e3bf810225e525835d677053b1b"}) - b.SetResolver("README.md", packr.Pointer{ForwardBox: gk, ForwardPath: "b2ab5cb21b5a72490b36cbd46cdfc102"}) - b.SetResolver("api/api.proto", packr.Pointer{ForwardBox: gk, ForwardPath: "bb9c567981605c082029d83839523a6c"}) - b.SetResolver("api/client.go.tmpl", packr.Pointer{ForwardBox: gk, ForwardPath: "4f6064f875a8245d8fccaa9a7fa8b27c"}) - b.SetResolver("cmd/main.go.tmpl", packr.Pointer{ForwardBox: gk, ForwardPath: "9b5deb2555f0ec914474cefd5111dc33"}) - b.SetResolver("configs/application.toml", packr.Pointer{ForwardBox: gk, ForwardPath: "70005e6633581fbd609bfec1957cd292"}) - b.SetResolver("configs/db.toml", packr.Pointer{ForwardBox: gk, ForwardPath: "2d07c6785bf36355d22a324ee7064328"}) - b.SetResolver("configs/grpc.toml", packr.Pointer{ForwardBox: gk, ForwardPath: "c00dc165bbf20a017bf129c26a1f8e7d"}) - b.SetResolver("configs/http.toml", packr.Pointer{ForwardBox: gk, ForwardPath: "360acf295547cde8b9a14a8d71f86bde"}) - b.SetResolver("configs/memcache.toml", packr.Pointer{ForwardBox: gk, ForwardPath: "e37cb3fefed2bf7aa681a848a63af049"}) - b.SetResolver("configs/redis.toml", packr.Pointer{ForwardBox: gk, ForwardPath: "2b96476f98cc5a0058f5fdd12ab432e1"}) - b.SetResolver("go.mod.tmpl", packr.Pointer{ForwardBox: gk, ForwardPath: "75fe549e4c6ec53880d899e12f303d4d"}) - b.SetResolver("internal/dao/dao.go.tmpl", packr.Pointer{ForwardBox: gk, ForwardPath: "67cc9f9db028b8b028595a334de05c39"}) - b.SetResolver("internal/dao/dao_test.go.tmpl", packr.Pointer{ForwardBox: gk, ForwardPath: "56df8426de024acea28c177fdbae2d79"}) - b.SetResolver("internal/dao/db.go.tmpl", packr.Pointer{ForwardBox: gk, ForwardPath: "deae3975927265bbe2e86537858be5d7"}) - b.SetResolver("internal/dao/mc.go.tmpl", packr.Pointer{ForwardBox: gk, ForwardPath: "91350508efec72331fa05cfb386fe8d3"}) - b.SetResolver("internal/dao/redis.go.tmpl", packr.Pointer{ForwardBox: gk, ForwardPath: "66133ffef1d9468dc3772e2c76e5a8d9"}) - b.SetResolver("internal/dao/wire.go.tmpl", packr.Pointer{ForwardBox: gk, ForwardPath: "d27273567e8a0cdf9c0f35717b89ff8d"}) - b.SetResolver("internal/di/app.go.tmpl", packr.Pointer{ForwardBox: gk, ForwardPath: "ece08322e1d8a72e1c014ac64ace18a9"}) - b.SetResolver("internal/di/wire.go.tmpl", packr.Pointer{ForwardBox: gk, ForwardPath: "0a6bc3e72bc7017e18eb65cbe3145be8"}) - b.SetResolver("internal/model/model.go.tmpl", packr.Pointer{ForwardBox: gk, ForwardPath: "07b6987724e01017288e376e48d717a4"}) - b.SetResolver("internal/server/grpc/server.go.tmpl", packr.Pointer{ForwardBox: gk, ForwardPath: "5f24168e1abae155c721ef6fa09bc429"}) - b.SetResolver("internal/server/http/server.go.tmpl", packr.Pointer{ForwardBox: gk, ForwardPath: "c1d37027fe90bb3a8ed6d3857637d326"}) - b.SetResolver("internal/service/service.go.tmpl", packr.Pointer{ForwardBox: gk, ForwardPath: "2bec6c6ed25a4c6dcdb30f32376e8814"}) - b.SetResolver("test/0_db.sql", packr.Pointer{ForwardBox: gk, ForwardPath: "9a2d5bf0f7319185c80da50bc1e9bb25"}) - b.SetResolver("test/1_data.sql", packr.Pointer{ForwardBox: gk, ForwardPath: "f5322c592d97a1cdc37376bd6c2bda44"}) - b.SetResolver("test/application.toml", packr.Pointer{ForwardBox: gk, ForwardPath: "8853489c373c278aef6338d0962fd746"}) - b.SetResolver("test/db.toml", packr.Pointer{ForwardBox: gk, ForwardPath: "f33c13a2bed49bfb098a354f3fb3d263"}) - b.SetResolver("test/docker-compose.yaml", packr.Pointer{ForwardBox: gk, ForwardPath: "344d3e90de055e3f730ec93c8d518442"}) - b.SetResolver("test/grpc.toml", packr.Pointer{ForwardBox: gk, ForwardPath: "519277b78f6f840309ab0251f7b54419"}) - b.SetResolver("test/http.toml", packr.Pointer{ForwardBox: gk, ForwardPath: "21390808875e3972b5fb30ef533f7595"}) - b.SetResolver("test/memcache.toml", packr.Pointer{ForwardBox: gk, ForwardPath: "50c9e9a3056e1c5519a3545096c6cd2e"}) - b.SetResolver("test/redis.toml", packr.Pointer{ForwardBox: gk, ForwardPath: "d48e95472ef22ab5c6e49c918ff4c7f7"}) - }() - - - func() { - b := packr.New("grpc", "./templates/grpc") - b.SetResolver("CHANGELOG.md", packr.Pointer{ForwardBox: gk, ForwardPath: "45b4b7a3d09ea9195265478d55919d04"}) - b.SetResolver("OWNERS", packr.Pointer{ForwardBox: gk, ForwardPath: "0babbb2bdef8b27731b8c6e569f74aa9"}) - b.SetResolver("README.md", packr.Pointer{ForwardBox: gk, ForwardPath: "b264df098d6769dab4f670828910fa60"}) - b.SetResolver("api/api.proto", packr.Pointer{ForwardBox: gk, ForwardPath: "7d1ce2614230661551a2feabde69da0a"}) - b.SetResolver("api/client.go.tmpl", packr.Pointer{ForwardBox: gk, ForwardPath: "2f0ce61d7311ba883ba12633891e37a5"}) - b.SetResolver("cmd/main.go.tmpl", packr.Pointer{ForwardBox: gk, ForwardPath: "0a985fe02d21f9d997ee7c01e1f60f82"}) - b.SetResolver("configs/application.toml", packr.Pointer{ForwardBox: gk, ForwardPath: "e2903d19bafc045ba9a6890ea9a43a52"}) - b.SetResolver("configs/db.toml", packr.Pointer{ForwardBox: gk, ForwardPath: "bac80cf86d8252f4a488b221da8cfe80"}) - b.SetResolver("configs/grpc.toml", packr.Pointer{ForwardBox: gk, ForwardPath: "34564973d460772fafff3026df19a783"}) - b.SetResolver("configs/memcache.toml", packr.Pointer{ForwardBox: gk, ForwardPath: "17e45e0f3497be1d7b615abf452cafa3"}) - b.SetResolver("configs/redis.toml", packr.Pointer{ForwardBox: gk, ForwardPath: "fd07d10d1e374aa9cdc1a9b1189e2f05"}) - b.SetResolver("go.mod.tmpl", packr.Pointer{ForwardBox: gk, ForwardPath: "240e2e99283e50af1f153a929e3d1116"}) - b.SetResolver("internal/dao/dao.go.tmpl", packr.Pointer{ForwardBox: gk, ForwardPath: "5700a76799052e1b831d6826b3f89cc6"}) - b.SetResolver("internal/dao/dao_test.go.tmpl", packr.Pointer{ForwardBox: gk, ForwardPath: "2e6074098bbcae09bb1bbbaf5c9c65d7"}) - b.SetResolver("internal/dao/db.go.tmpl", packr.Pointer{ForwardBox: gk, ForwardPath: "01947c6dd922af83263605f0243198da"}) - b.SetResolver("internal/dao/mc.go.tmpl", packr.Pointer{ForwardBox: gk, ForwardPath: "f02c908885e29d26bfb4de9c33c5e4d1"}) - b.SetResolver("internal/dao/redis.go.tmpl", packr.Pointer{ForwardBox: gk, ForwardPath: "d11e25ce5264ce1ed0f0863d28fa7113"}) - b.SetResolver("internal/dao/wire.go.tmpl", packr.Pointer{ForwardBox: gk, ForwardPath: "0c5ba5fa8837f2def63c05cc79c53394"}) - b.SetResolver("internal/di/app.go.tmpl", packr.Pointer{ForwardBox: gk, ForwardPath: "ca7acd4efcb673fe4e3505be171752b7"}) - b.SetResolver("internal/di/wire.go.tmpl", packr.Pointer{ForwardBox: gk, ForwardPath: "f00e9b617b63f2dc20a592cf7aeb0f8a"}) - b.SetResolver("internal/model/model.go.tmpl", packr.Pointer{ForwardBox: gk, ForwardPath: "1a41024e60169dfd892e57d36264a7e3"}) - b.SetResolver("internal/server/grpc/server.go.tmpl", packr.Pointer{ForwardBox: gk, ForwardPath: "f8007e4822e340ca304d913fcd9d783b"}) - b.SetResolver("internal/service/service.go.tmpl", packr.Pointer{ForwardBox: gk, ForwardPath: "ef3624e258287c266b0982f7c486d27b"}) - b.SetResolver("test/0_db.sql", packr.Pointer{ForwardBox: gk, ForwardPath: "47c5a1821e7054758d52c8004a7033de"}) - b.SetResolver("test/1_data.sql", packr.Pointer{ForwardBox: gk, ForwardPath: "79906f2027d86895882c07b014541636"}) - b.SetResolver("test/application.toml", packr.Pointer{ForwardBox: gk, ForwardPath: "69622533133ad8df518fe01a940713ec"}) - b.SetResolver("test/db.toml", packr.Pointer{ForwardBox: gk, ForwardPath: "5ba5532a198294fc750ba2f01006d39b"}) - b.SetResolver("test/docker-compose.yaml", packr.Pointer{ForwardBox: gk, ForwardPath: "ad071804417ba1d9b2afe916414fa2e0"}) - b.SetResolver("test/grpc.toml", packr.Pointer{ForwardBox: gk, ForwardPath: "d6bf2490901d7994259b59036e5ee787"}) - b.SetResolver("test/memcache.toml", packr.Pointer{ForwardBox: gk, ForwardPath: "7a430917a7aa07186808d95369458c4e"}) - b.SetResolver("test/redis.toml", packr.Pointer{ForwardBox: gk, ForwardPath: "fda1b2ac9058723473a3ae92bef45575"}) - }() - - - func() { - b := packr.New("http", "./templates/http") - b.SetResolver("CHANGELOG.md", packr.Pointer{ForwardBox: gk, ForwardPath: "e14aca05bd4c126f7220c63941266e17"}) - b.SetResolver("OWNERS", packr.Pointer{ForwardBox: gk, ForwardPath: "a9cbe2e1020ca7be3617cd6d83fe88f5"}) - b.SetResolver("README.md", packr.Pointer{ForwardBox: gk, ForwardPath: "ddd2d8d002a9cac8b4c670cb20f96f3c"}) - b.SetResolver("api/api.proto", packr.Pointer{ForwardBox: gk, ForwardPath: "ae6a3278e56d79251f55ea74dcd489a8"}) - b.SetResolver("api/client.go.tmpl", packr.Pointer{ForwardBox: gk, ForwardPath: "59b3f5316758be0d8bdfb8150516578a"}) - b.SetResolver("cmd/main.go.tmpl", packr.Pointer{ForwardBox: gk, ForwardPath: "47f7fee79587764108735a0e37a3f1cb"}) - b.SetResolver("configs/application.toml", packr.Pointer{ForwardBox: gk, ForwardPath: "450e380f3fbd9366ee660538f5f4eb47"}) - b.SetResolver("configs/db.toml", packr.Pointer{ForwardBox: gk, ForwardPath: "7c8bcba5a2dfe4aa3bb18e6ee7c91140"}) - b.SetResolver("configs/http.toml", packr.Pointer{ForwardBox: gk, ForwardPath: "3deb9e59e02e8c5bc6ba59a66141508f"}) - b.SetResolver("configs/memcache.toml", packr.Pointer{ForwardBox: gk, ForwardPath: "7b856fed41dcc4df47597de661eb9042"}) - b.SetResolver("configs/redis.toml", packr.Pointer{ForwardBox: gk, ForwardPath: "7ba5a6706dc47731b76a9fccdc393d28"}) - b.SetResolver("go.mod.tmpl", packr.Pointer{ForwardBox: gk, ForwardPath: "d391eec375a18e4524decc911a55cb37"}) - b.SetResolver("internal/dao/dao.go.tmpl", packr.Pointer{ForwardBox: gk, ForwardPath: "c84c3b26a959752d7eb926d02ce46ddd"}) - b.SetResolver("internal/dao/dao_test.go.tmpl", packr.Pointer{ForwardBox: gk, ForwardPath: "710a08c10a56dd632085fe2723c2fdca"}) - b.SetResolver("internal/dao/db.go.tmpl", packr.Pointer{ForwardBox: gk, ForwardPath: "28edc15b141434022bafc70e865e28c3"}) - b.SetResolver("internal/dao/mc.go.tmpl", packr.Pointer{ForwardBox: gk, ForwardPath: "596e234fc3c0d58dd019eff083dc005f"}) - b.SetResolver("internal/dao/redis.go.tmpl", packr.Pointer{ForwardBox: gk, ForwardPath: "d9e24a8fbdaa656b04d082a11a16fab2"}) - b.SetResolver("internal/dao/wire.go.tmpl", packr.Pointer{ForwardBox: gk, ForwardPath: "dc9ddbcd9cdbb28fa4632260727f292c"}) - b.SetResolver("internal/di/app.go.tmpl", packr.Pointer{ForwardBox: gk, ForwardPath: "991f8014a5f12bbf1f53265c7d62afcb"}) - b.SetResolver("internal/di/wire.go.tmpl", packr.Pointer{ForwardBox: gk, ForwardPath: "2c5e0d8471a941cbc1b0532422997425"}) - b.SetResolver("internal/model/model.go.tmpl", packr.Pointer{ForwardBox: gk, ForwardPath: "e4ce7202d112d82a5e014831a14d1ba5"}) - b.SetResolver("internal/server/http/server.go.tmpl", packr.Pointer{ForwardBox: gk, ForwardPath: "e5e2572a852f6474077b82fb7793df25"}) - b.SetResolver("internal/service/service.go.tmpl", packr.Pointer{ForwardBox: gk, ForwardPath: "0800ba3c38cf5e9bf163d71215211839"}) - b.SetResolver("test/0_db.sql", packr.Pointer{ForwardBox: gk, ForwardPath: "c5dc8e7e6833a138a5738b8d0eaa4040"}) - b.SetResolver("test/1_data.sql", packr.Pointer{ForwardBox: gk, ForwardPath: "bcd84391b0e45f9d8641de9b0f6bdc98"}) - b.SetResolver("test/application.toml", packr.Pointer{ForwardBox: gk, ForwardPath: "3fcb8456a23b643f9cd2b5df95e3e497"}) - b.SetResolver("test/db.toml", packr.Pointer{ForwardBox: gk, ForwardPath: "e141e2657bd9e59d6741ab94b976c8d9"}) - b.SetResolver("test/docker-compose.yaml", packr.Pointer{ForwardBox: gk, ForwardPath: "91045d262e11302068a4d78ac8a2912e"}) - b.SetResolver("test/http.toml", packr.Pointer{ForwardBox: gk, ForwardPath: "7b2c3c96383cfe733a24c5ae20b892e8"}) - b.SetResolver("test/memcache.toml", packr.Pointer{ForwardBox: gk, ForwardPath: "1dc54a258d1b22873bd8a065ef5f5136"}) - b.SetResolver("test/redis.toml", packr.Pointer{ForwardBox: gk, ForwardPath: "f44350500991f17cec6420d8300b4f3d"}) - }() - - return nil -}() diff --git a/tool/kratos-gen-project/project.go b/tool/kratos-gen-project/project.go deleted file mode 100644 index b9025d180..000000000 --- a/tool/kratos-gen-project/project.go +++ /dev/null @@ -1,96 +0,0 @@ -package main - -import ( - "bytes" - "io/ioutil" - "os" - "os/exec" - "path/filepath" - "strings" - "text/template" - - "github.com/gobuffalo/packr/v2" -) - -// project project config -type project struct { - // project name - Name string - // mod prefix - ModPrefix string - // project dir - path string - none bool - onlyGRPC bool - onlyHTTP bool -} - -var p project - -//go:generate packr2 -func create() (err error) { - box := packr.New("all", "./templates/all") - if p.onlyHTTP { - box = packr.New("http", "./templates/http") - } else if p.onlyGRPC { - box = packr.New("grpc", "./templates/grpc") - } - if err = os.MkdirAll(p.path, 0755); err != nil { - return - } - for _, name := range box.List() { - if p.ModPrefix != "" && name == "go.mod.tmpl" { - continue - } - tmpl, _ := box.FindString(name) - i := strings.LastIndex(name, string(os.PathSeparator)) - if i > 0 { - dir := name[:i] - if err = os.MkdirAll(filepath.Join(p.path, dir), 0755); err != nil { - return - } - } - if strings.HasSuffix(name, ".tmpl") { - name = strings.TrimSuffix(name, ".tmpl") - } - if err = write(filepath.Join(p.path, name), tmpl); err != nil { - return - } - } - - if err = generate("./..."); err != nil { - return - } - if err = generate("./internal/dao/wire.go"); err != nil { - return - } - return -} - -func generate(path string) error { - cmd := exec.Command("go", "generate", "-x", path) - cmd.Dir = p.path - cmd.Stdout = os.Stdout - cmd.Stderr = os.Stderr - return cmd.Run() -} - -func write(path, tpl string) (err error) { - data, err := parse(tpl) - if err != nil { - return - } - return ioutil.WriteFile(path, data, 0644) -} - -func parse(s string) ([]byte, error) { - t, err := template.New("").Parse(s) - if err != nil { - return nil, err - } - var buf bytes.Buffer - if err = t.Execute(&buf, p); err != nil { - return nil, err - } - return buf.Bytes(), nil -} diff --git a/tool/kratos-gen-project/templates/all/CHANGELOG.md b/tool/kratos-gen-project/templates/all/CHANGELOG.md deleted file mode 100644 index c39acc0e2..000000000 --- a/tool/kratos-gen-project/templates/all/CHANGELOG.md +++ /dev/null @@ -1,4 +0,0 @@ -## Demo - -### v1.0.0 -1. 上线功能xxx diff --git a/tool/kratos-gen-project/templates/all/OWNERS b/tool/kratos-gen-project/templates/all/OWNERS deleted file mode 100644 index c1e28c554..000000000 --- a/tool/kratos-gen-project/templates/all/OWNERS +++ /dev/null @@ -1,2 +0,0 @@ -# Author -# Reviewer diff --git a/tool/kratos-gen-project/templates/all/README.md b/tool/kratos-gen-project/templates/all/README.md deleted file mode 100644 index e43f93fc3..000000000 --- a/tool/kratos-gen-project/templates/all/README.md +++ /dev/null @@ -1,4 +0,0 @@ -# Demo - -## 项目简介 -1. diff --git a/tool/kratos-gen-project/templates/all/api/api.proto b/tool/kratos-gen-project/templates/all/api/api.proto deleted file mode 100644 index 0ba83f9f5..000000000 --- a/tool/kratos-gen-project/templates/all/api/api.proto +++ /dev/null @@ -1,34 +0,0 @@ -// 定义项目 API 的 proto 文件 可以同时描述 gRPC 和 HTTP API -// protobuf 文件参考: -// - https://developers.google.com/protocol-buffers/ -syntax = "proto3"; - -import "github.com/gogo/protobuf/gogoproto/gogo.proto"; -import "google/protobuf/empty.proto"; -import "google/api/annotations.proto"; - -// package 命名使用 {appid}.{version} 的方式, version 形如 v1, v2 .. -package demo.service.v1; - -// NOTE: 最后请删除这些无用的注释 (゜-゜)つロ - -option go_package = "api"; -option (gogoproto.goproto_getters_all) = false; - -service Demo { - rpc Ping(.google.protobuf.Empty) returns (.google.protobuf.Empty); - rpc SayHello(HelloReq) returns (.google.protobuf.Empty); - rpc SayHelloURL(HelloReq) returns (HelloResp) { - option (google.api.http) = { - get: "/kratos-demo/say_hello" - }; - }; -} - -message HelloReq { - string name = 1 [(gogoproto.moretags) = 'form:"name" validate:"required"']; -} - -message HelloResp { - string Content = 1 [(gogoproto.jsontag) = 'content']; -} diff --git a/tool/kratos-gen-project/templates/all/api/client.go.tmpl b/tool/kratos-gen-project/templates/all/api/client.go.tmpl deleted file mode 100644 index f3a3b2083..000000000 --- a/tool/kratos-gen-project/templates/all/api/client.go.tmpl +++ /dev/null @@ -1,25 +0,0 @@ -package api -import ( - "context" - "fmt" - - "github.com/go-kratos/kratos/pkg/net/rpc/warden" - - "google.golang.org/grpc" -) - -// AppID . -const AppID = "TODO: ADD APP ID" - -// NewClient new grpc client -func NewClient(cfg *warden.ClientConfig, opts ...grpc.DialOption) (DemoClient, error) { - client := warden.NewClient(cfg, opts...) - cc, err := client.Dial(context.Background(), fmt.Sprintf("discovery://default/%s", AppID)) - if err != nil { - return nil, err - } - return NewDemoClient(cc), nil -} - -// 生成 gRPC 代码 -//go:generate kratos tool protoc --grpc --bm api.proto diff --git a/tool/kratos-gen-project/templates/all/cmd/main.go.tmpl b/tool/kratos-gen-project/templates/all/cmd/main.go.tmpl deleted file mode 100644 index de1aefec2..000000000 --- a/tool/kratos-gen-project/templates/all/cmd/main.go.tmpl +++ /dev/null @@ -1,41 +0,0 @@ -package main - -import ( - "flag" - "os" - "os/signal" - "syscall" - "time" - - "{{.ModPrefix}}{{.Name}}/internal/di" - "github.com/go-kratos/kratos/pkg/conf/paladin" - "github.com/go-kratos/kratos/pkg/log" -) - -func main() { - flag.Parse() - log.Init(nil) // debug flag: log.dir={path} - defer log.Close() - log.Info("{{.Name}} start") - paladin.Init() - _, closeFunc, err := di.InitApp() - if err != nil { - panic(err) - } - c := make(chan os.Signal, 1) - signal.Notify(c, syscall.SIGHUP, syscall.SIGQUIT, syscall.SIGTERM, syscall.SIGINT) - for { - s := <-c - log.Info("get a signal %s", s.String()) - switch s { - case syscall.SIGQUIT, syscall.SIGTERM, syscall.SIGINT: - closeFunc() - log.Info("{{.Name}} exit") - time.Sleep(time.Second) - return - case syscall.SIGHUP: - default: - return - } - } -} diff --git a/tool/kratos-gen-project/templates/all/configs/application.toml b/tool/kratos-gen-project/templates/all/configs/application.toml deleted file mode 100644 index a42ca6e63..000000000 --- a/tool/kratos-gen-project/templates/all/configs/application.toml +++ /dev/null @@ -1,3 +0,0 @@ - -# This is a TOML document. Boom~ -demoExpire = "24h" diff --git a/tool/kratos-gen-project/templates/all/configs/db.toml b/tool/kratos-gen-project/templates/all/configs/db.toml deleted file mode 100644 index 840bd2e78..000000000 --- a/tool/kratos-gen-project/templates/all/configs/db.toml +++ /dev/null @@ -1,10 +0,0 @@ -[Client] - addr = "127.0.0.1:3306" - dsn = "{user}:{password}@tcp(127.0.0.1:3306)/{database}?timeout=1s&readTimeout=1s&writeTimeout=1s&parseTime=true&loc=Local&charset=utf8mb4,utf8" - readDSN = ["{user}:{password}@tcp(127.0.0.2:3306)/{database}?timeout=1s&readTimeout=1s&writeTimeout=1s&parseTime=true&loc=Local&charset=utf8mb4,utf8","{user}:{password}@tcp(127.0.0.3:3306)/{database}?timeout=1s&readTimeout=1s&writeTimeout=1s&parseTime=true&loc=Local&charset=utf8,utf8mb4"] - active = 20 - idle = 10 - idleTimeout ="4h" - queryTimeout = "200ms" - execTimeout = "300ms" - tranTimeout = "400ms" diff --git a/tool/kratos-gen-project/templates/all/configs/grpc.toml b/tool/kratos-gen-project/templates/all/configs/grpc.toml deleted file mode 100644 index 40339cecc..000000000 --- a/tool/kratos-gen-project/templates/all/configs/grpc.toml +++ /dev/null @@ -1,3 +0,0 @@ -[Server] - addr = "0.0.0.0:9000" - timeout = "1s" diff --git a/tool/kratos-gen-project/templates/all/configs/http.toml b/tool/kratos-gen-project/templates/all/configs/http.toml deleted file mode 100644 index 951a03197..000000000 --- a/tool/kratos-gen-project/templates/all/configs/http.toml +++ /dev/null @@ -1,3 +0,0 @@ -[Server] - addr = "0.0.0.0:8000" - timeout = "1s" diff --git a/tool/kratos-gen-project/templates/all/configs/memcache.toml b/tool/kratos-gen-project/templates/all/configs/memcache.toml deleted file mode 100644 index 0f2b900d3..000000000 --- a/tool/kratos-gen-project/templates/all/configs/memcache.toml +++ /dev/null @@ -1,10 +0,0 @@ -[Client] - name = "demo" - proto = "tcp" - addr = "127.0.0.1:11211" - active = 50 - idle = 10 - dialTimeout = "100ms" - readTimeout = "200ms" - writeTimeout = "300ms" - idleTimeout = "80s" diff --git a/tool/kratos-gen-project/templates/all/configs/redis.toml b/tool/kratos-gen-project/templates/all/configs/redis.toml deleted file mode 100644 index d07950de0..000000000 --- a/tool/kratos-gen-project/templates/all/configs/redis.toml +++ /dev/null @@ -1,10 +0,0 @@ -[Client] - name = "demo" - proto = "tcp" - addr = "127.0.0.1:6379" - idle = 10 - active = 10 - dialTimeout = "1s" - readTimeout = "1s" - writeTimeout = "1s" - idleTimeout = "10s" diff --git a/tool/kratos-gen-project/templates/all/go.mod.tmpl b/tool/kratos-gen-project/templates/all/go.mod.tmpl deleted file mode 100644 index fc8719d34..000000000 --- a/tool/kratos-gen-project/templates/all/go.mod.tmpl +++ /dev/null @@ -1,12 +0,0 @@ -module {{.Name}} - -go 1.13 - -require ( - github.com/go-kratos/kratos master - github.com/gogo/protobuf v1.2.1 - github.com/golang/protobuf v1.3.2 - golang.org/x/net v0.0.0-20190628185345-da137c7871d7 - google.golang.org/grpc v1.28.1 -) - diff --git a/tool/kratos-gen-project/templates/all/internal/dao/dao.go.tmpl b/tool/kratos-gen-project/templates/all/internal/dao/dao.go.tmpl deleted file mode 100644 index 81ea0b540..000000000 --- a/tool/kratos-gen-project/templates/all/internal/dao/dao.go.tmpl +++ /dev/null @@ -1,69 +0,0 @@ -package dao - -import ( - "context" - "time" - - "{{.ModPrefix}}{{.Name}}/internal/model" - "github.com/go-kratos/kratos/pkg/cache/memcache" - "github.com/go-kratos/kratos/pkg/cache/redis" - "github.com/go-kratos/kratos/pkg/conf/paladin" - "github.com/go-kratos/kratos/pkg/database/sql" - "github.com/go-kratos/kratos/pkg/sync/pipeline/fanout" - xtime "github.com/go-kratos/kratos/pkg/time" - - "github.com/google/wire" -) - -var Provider = wire.NewSet(New, NewDB, NewRedis, NewMC) - -//go:generate kratos tool genbts -// Dao dao interface -type Dao interface { - Close() - Ping(ctx context.Context) (err error) - // bts: -nullcache=&model.Article{ID:-1} -check_null_code=$!=nil&&$.ID==-1 - RawArticle(c context.Context, id int64) (*model.Article, error) -} - -// dao dao. -type dao struct { - db *sql.DB - redis *redis.Redis - mc *memcache.Memcache - cache *fanout.Fanout - demoExpire int32 -} - -// New new a dao and return. -func New(r *redis.Redis, mc *memcache.Memcache, db *sql.DB) (d Dao, cf func(), err error) { - return newDao(r, mc, db) -} - -func newDao(r *redis.Redis, mc *memcache.Memcache, db *sql.DB) (d *dao, cf func(), err error) { - var cfg struct{ - DemoExpire xtime.Duration - } - if err = paladin.Get("application.toml").UnmarshalTOML(&cfg); err != nil { - return - } - d = &dao{ - db: db, - redis: r, - mc: mc, - cache: fanout.New("cache"), - demoExpire: int32(time.Duration(cfg.DemoExpire) / time.Second), - } - cf = d.Close - return -} - -// Close close the resource. -func (d *dao) Close() { - d.cache.Close() -} - -// Ping ping the resource. -func (d *dao) Ping(ctx context.Context) (err error) { - return nil -} diff --git a/tool/kratos-gen-project/templates/all/internal/dao/dao_test.go.tmpl b/tool/kratos-gen-project/templates/all/internal/dao/dao_test.go.tmpl deleted file mode 100644 index 90849e012..000000000 --- a/tool/kratos-gen-project/templates/all/internal/dao/dao_test.go.tmpl +++ /dev/null @@ -1,40 +0,0 @@ -package dao - -import ( - "context" - "flag" - "os" - "testing" - - "github.com/go-kratos/kratos/pkg/conf/paladin" - "github.com/go-kratos/kratos/pkg/testing/lich" -) - -var d *dao -var ctx = context.Background() - -func TestMain(m *testing.M) { - flag.Set("conf", "../../test") - flag.Set("f", "../../test/docker-compose.yaml") - flag.Parse() - disableLich := os.Getenv("DISABLE_LICH") != "" - if !disableLich { - if err := lich.Setup(); err != nil { - panic(err) - } - } - var err error - if err = paladin.Init(); err != nil { - panic(err) - } - var cf func() - if d, cf, err = newTestDao();err != nil { - panic(err) - } - ret := m.Run() - cf() - if !disableLich { - _ = lich.Teardown() - } - os.Exit(ret) -} diff --git a/tool/kratos-gen-project/templates/all/internal/dao/db.go.tmpl b/tool/kratos-gen-project/templates/all/internal/dao/db.go.tmpl deleted file mode 100644 index b3bde3064..000000000 --- a/tool/kratos-gen-project/templates/all/internal/dao/db.go.tmpl +++ /dev/null @@ -1,30 +0,0 @@ -package dao - -import ( - "context" - - "{{.ModPrefix}}{{.Name}}/internal/model" - "github.com/go-kratos/kratos/pkg/conf/paladin" - "github.com/go-kratos/kratos/pkg/database/sql" -) - -func NewDB() (db *sql.DB, cf func(), err error) { - var ( - cfg sql.Config - ct paladin.TOML - ) - if err = paladin.Get("db.toml").Unmarshal(&ct); err != nil { - return - } - if err = ct.Get("Client").UnmarshalTOML(&cfg); err != nil { - return - } - db = sql.NewMySQL(&cfg) - cf = func() {db.Close()} - return -} - -func (d *dao) RawArticle(ctx context.Context, id int64) (art *model.Article, err error) { - // get data from db - return -} diff --git a/tool/kratos-gen-project/templates/all/internal/dao/mc.go.tmpl b/tool/kratos-gen-project/templates/all/internal/dao/mc.go.tmpl deleted file mode 100644 index 44cfdf13b..000000000 --- a/tool/kratos-gen-project/templates/all/internal/dao/mc.go.tmpl +++ /dev/null @@ -1,48 +0,0 @@ -package dao - -import ( - "context" - "fmt" - - "{{.ModPrefix}}{{.Name}}/internal/model" - "github.com/go-kratos/kratos/pkg/cache/memcache" - "github.com/go-kratos/kratos/pkg/conf/paladin" - "github.com/go-kratos/kratos/pkg/log" -) - -//go:generate kratos tool genmc -type _mc interface { - // mc: -key=keyArt -type=get - CacheArticle(c context.Context, id int64) (*model.Article, error) - // mc: -key=keyArt -expire=d.demoExpire - AddCacheArticle(c context.Context, id int64, art *model.Article) (err error) - // mc: -key=keyArt - DeleteArticleCache(c context.Context, id int64) (err error) -} - -func NewMC() (mc *memcache.Memcache, cf func(), err error) { - var ( - cfg memcache.Config - ct paladin.TOML - ) - if err = paladin.Get("memcache.toml").Unmarshal(&ct); err != nil { - return - } - if err = ct.Get("Client").UnmarshalTOML(&cfg); err != nil { - return - } - mc = memcache.New(&cfg) - cf = func() {mc.Close()} - return -} - -func (d *dao) PingMC(ctx context.Context) (err error) { - if err = d.mc.Set(ctx, &memcache.Item{Key: "ping", Value: []byte("pong"), Expiration: 0}); err != nil { - log.Error("conn.Set(PING) error(%v)", err) - } - return -} - -func keyArt(id int64) string { - return fmt.Sprintf("art_%d", id) -} diff --git a/tool/kratos-gen-project/templates/all/internal/dao/redis.go.tmpl b/tool/kratos-gen-project/templates/all/internal/dao/redis.go.tmpl deleted file mode 100644 index e3c962eee..000000000 --- a/tool/kratos-gen-project/templates/all/internal/dao/redis.go.tmpl +++ /dev/null @@ -1,32 +0,0 @@ -package dao - -import ( - "context" - - "github.com/go-kratos/kratos/pkg/cache/redis" - "github.com/go-kratos/kratos/pkg/conf/paladin" - "github.com/go-kratos/kratos/pkg/log" -) - -func NewRedis() (r *redis.Redis, cf func(), err error) { - var ( - cfg redis.Config - ct paladin.Map - ) - if err = paladin.Get("redis.toml").Unmarshal(&ct); err != nil { - return - } - if err = ct.Get("Client").UnmarshalTOML(&cfg); err != nil { - return - } - r = redis.NewRedis(&cfg) - cf = func(){r.Close()} - return -} - -func (d *dao) PingRedis(ctx context.Context) (err error) { - if _, err = d.redis.Do(ctx, "SET", "ping", "pong"); err != nil { - log.Error("conn.Set(PING) error(%v)", err) - } - return -} \ No newline at end of file diff --git a/tool/kratos-gen-project/templates/all/internal/dao/wire.go.tmpl b/tool/kratos-gen-project/templates/all/internal/dao/wire.go.tmpl deleted file mode 100644 index a00b2d39a..000000000 --- a/tool/kratos-gen-project/templates/all/internal/dao/wire.go.tmpl +++ /dev/null @@ -1,13 +0,0 @@ -// +build wireinject -// The build tag makes sure the stub is not built in the final build. - -package dao - -import ( - "github.com/google/wire" -) - -//go:generate kratos tool wire -func newTestDao() (*dao, func(), error) { - panic(wire.Build(newDao, NewDB, NewRedis, NewMC)) -} diff --git a/tool/kratos-gen-project/templates/all/internal/di/app.go.tmpl b/tool/kratos-gen-project/templates/all/internal/di/app.go.tmpl deleted file mode 100644 index 14e2d2a71..000000000 --- a/tool/kratos-gen-project/templates/all/internal/di/app.go.tmpl +++ /dev/null @@ -1,38 +0,0 @@ -package di - -import ( - "context" - "time" - - "{{.ModPrefix}}{{.Name}}/internal/service" - - "github.com/go-kratos/kratos/pkg/log" - bm "github.com/go-kratos/kratos/pkg/net/http/blademaster" - "github.com/go-kratos/kratos/pkg/net/rpc/warden" -) - -//go:generate kratos tool wire -type App struct { - svc *service.Service - http *bm.Engine - grpc *warden.Server -} - -func NewApp(svc *service.Service, h *bm.Engine, g *warden.Server) (app *App, closeFunc func(), err error){ - app = &App{ - svc: svc, - http: h, - grpc: g, - } - closeFunc = func() { - ctx, cancel := context.WithTimeout(context.Background(), 35*time.Second) - if err := g.Shutdown(ctx); err != nil { - log.Error("grpcSrv.Shutdown error(%v)", err) - } - if err := h.Shutdown(ctx); err != nil { - log.Error("httpSrv.Shutdown error(%v)", err) - } - cancel() - } - return -} diff --git a/tool/kratos-gen-project/templates/all/internal/di/wire.go.tmpl b/tool/kratos-gen-project/templates/all/internal/di/wire.go.tmpl deleted file mode 100644 index 9ef04b19b..000000000 --- a/tool/kratos-gen-project/templates/all/internal/di/wire.go.tmpl +++ /dev/null @@ -1,18 +0,0 @@ -// +build wireinject -// The build tag makes sure the stub is not built in the final build. - -package di - -import ( - "{{.ModPrefix}}{{.Name}}/internal/dao" - "{{.ModPrefix}}{{.Name}}/internal/service" - "{{.ModPrefix}}{{.Name}}/internal/server/grpc" - "{{.ModPrefix}}{{.Name}}/internal/server/http" - - "github.com/google/wire" -) - -//go:generate kratos t wire -func InitApp() (*App, func(), error) { - panic(wire.Build(dao.Provider, service.Provider, http.New, grpc.New, NewApp)) -} diff --git a/tool/kratos-gen-project/templates/all/internal/model/model.go.tmpl b/tool/kratos-gen-project/templates/all/internal/model/model.go.tmpl deleted file mode 100644 index b3fcf7985..000000000 --- a/tool/kratos-gen-project/templates/all/internal/model/model.go.tmpl +++ /dev/null @@ -1,12 +0,0 @@ -package model - -// Kratos hello kratos. -type Kratos struct { - Hello string -} - -type Article struct { - ID int64 - Content string - Author string -} \ No newline at end of file diff --git a/tool/kratos-gen-project/templates/all/internal/server/grpc/server.go.tmpl b/tool/kratos-gen-project/templates/all/internal/server/grpc/server.go.tmpl deleted file mode 100644 index 4db767635..000000000 --- a/tool/kratos-gen-project/templates/all/internal/server/grpc/server.go.tmpl +++ /dev/null @@ -1,26 +0,0 @@ -package grpc - -import ( - pb "{{.ModPrefix}}{{.Name}}/api" - - "github.com/go-kratos/kratos/pkg/conf/paladin" - "github.com/go-kratos/kratos/pkg/net/rpc/warden" -) - -// New new a grpc server. -func New(svc pb.DemoServer) (ws *warden.Server, err error) { - var ( - cfg warden.ServerConfig - ct paladin.TOML - ) - if err = paladin.Get("grpc.toml").Unmarshal(&ct); err != nil { - return - } - if err = ct.Get("Server").UnmarshalTOML(&cfg); err != nil { - return - } - ws = warden.NewServer(&cfg) - pb.RegisterDemoServer(ws.Server(), svc) - ws, err = ws.Start() - return -} diff --git a/tool/kratos-gen-project/templates/all/internal/server/http/server.go.tmpl b/tool/kratos-gen-project/templates/all/internal/server/http/server.go.tmpl deleted file mode 100644 index 3bec93a28..000000000 --- a/tool/kratos-gen-project/templates/all/internal/server/http/server.go.tmpl +++ /dev/null @@ -1,56 +0,0 @@ -package http - -import ( - "net/http" - - pb "{{.ModPrefix}}{{.Name}}/api" - "{{.ModPrefix}}{{.Name}}/internal/model" - "github.com/go-kratos/kratos/pkg/conf/paladin" - "github.com/go-kratos/kratos/pkg/log" - bm "github.com/go-kratos/kratos/pkg/net/http/blademaster" -) - -var svc pb.DemoServer - -// New new a bm server. -func New(s pb.DemoServer) (engine *bm.Engine, err error) { - var ( - cfg bm.ServerConfig - ct paladin.TOML - ) - if err = paladin.Get("http.toml").Unmarshal(&ct); err != nil { - return - } - if err = ct.Get("Server").UnmarshalTOML(&cfg); err != nil { - return - } - svc = s - engine = bm.DefaultServer(&cfg) - pb.RegisterDemoBMServer(engine, s) - initRouter(engine) - err = engine.Start() - return -} - -func initRouter(e *bm.Engine) { - e.Ping(ping) - g := e.Group("/{{.Name}}") - { - g.GET("/start", howToStart) - } -} - -func ping(ctx *bm.Context) { - if _, err := svc.Ping(ctx, nil); err != nil { - log.Error("ping error(%v)", err) - ctx.AbortWithStatus(http.StatusServiceUnavailable) - } -} - -// example for http request handler. -func howToStart(c *bm.Context) { - k := &model.Kratos{ - Hello: "Golang 大法好 !!!", - } - c.JSON(k, nil) -} \ No newline at end of file diff --git a/tool/kratos-gen-project/templates/all/internal/service/service.go.tmpl b/tool/kratos-gen-project/templates/all/internal/service/service.go.tmpl deleted file mode 100644 index f3750cc69..000000000 --- a/tool/kratos-gen-project/templates/all/internal/service/service.go.tmpl +++ /dev/null @@ -1,57 +0,0 @@ -package service - -import ( - "context" - "fmt" - - pb "{{.ModPrefix}}{{.Name}}/api" - "{{.ModPrefix}}{{.Name}}/internal/dao" - "github.com/go-kratos/kratos/pkg/conf/paladin" - - "github.com/golang/protobuf/ptypes/empty" - "github.com/google/wire" -) - -var Provider = wire.NewSet(New, wire.Bind(new(pb.DemoServer), new(*Service))) - -// Service service. -type Service struct { - ac *paladin.Map - dao dao.Dao -} - -// New new a service and return. -func New(d dao.Dao) (s *Service, cf func(), err error) { - s = &Service{ - ac: &paladin.TOML{}, - dao: d, - } - cf = s.Close - err = paladin.Watch("application.toml", s.ac) - return -} - -// SayHello grpc demo func. -func (s *Service) SayHello(ctx context.Context, req *pb.HelloReq) (reply *empty.Empty, err error) { - reply = new(empty.Empty) - fmt.Printf("hello %s", req.Name) - return -} - -// SayHelloURL bm demo func. -func (s *Service) SayHelloURL(ctx context.Context, req *pb.HelloReq) (reply *pb.HelloResp, err error) { - reply = &pb.HelloResp{ - Content: "hello " + req.Name, - } - fmt.Printf("hello url %s", req.Name) - return -} - -// Ping ping the resource. -func (s *Service) Ping(ctx context.Context, e *empty.Empty) (*empty.Empty, error) { - return &empty.Empty{}, s.dao.Ping(ctx) -} - -// Close close the resource. -func (s *Service) Close() { -} diff --git a/tool/kratos-gen-project/templates/all/test/0_db.sql b/tool/kratos-gen-project/templates/all/test/0_db.sql deleted file mode 100644 index e2cbccc19..000000000 --- a/tool/kratos-gen-project/templates/all/test/0_db.sql +++ /dev/null @@ -1,11 +0,0 @@ -create database kratos_demo; -use kratos_demo; - -CREATE TABLE `articles` ( - `id` int(11) UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '主键ID', - `title` varchar(64) NOT NULL COMMENT '名称', - `mtime` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '最后修改时间', - `ctime` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', - PRIMARY KEY (`id`), - KEY `ix_mtime` (`mtime`) -) COMMENT='文章表'; diff --git a/tool/kratos-gen-project/templates/all/test/1_data.sql b/tool/kratos-gen-project/templates/all/test/1_data.sql deleted file mode 100644 index c7cb57167..000000000 --- a/tool/kratos-gen-project/templates/all/test/1_data.sql +++ /dev/null @@ -1,3 +0,0 @@ -use kratos_demo; - -INSERT INTO articles(`id`, `title`) VALUES (1, 'title'); diff --git a/tool/kratos-gen-project/templates/all/test/application.toml b/tool/kratos-gen-project/templates/all/test/application.toml deleted file mode 100644 index a42ca6e63..000000000 --- a/tool/kratos-gen-project/templates/all/test/application.toml +++ /dev/null @@ -1,3 +0,0 @@ - -# This is a TOML document. Boom~ -demoExpire = "24h" diff --git a/tool/kratos-gen-project/templates/all/test/db.toml b/tool/kratos-gen-project/templates/all/test/db.toml deleted file mode 100644 index 9c7bd8618..000000000 --- a/tool/kratos-gen-project/templates/all/test/db.toml +++ /dev/null @@ -1,8 +0,0 @@ -[Client] - dsn = "root:root@tcp(127.0.0.1:13306)/kratos_demo?timeout=1s&readTimeout=1s&writeTimeout=1s&parseTime=true&loc=Local&charset=utf8mb4,utf8" - active = 20 - idle = 10 - idleTimeout ="4h" - queryTimeout = "200ms" - execTimeout = "300ms" - tranTimeout = "400ms" diff --git a/tool/kratos-gen-project/templates/all/test/docker-compose.yaml b/tool/kratos-gen-project/templates/all/test/docker-compose.yaml deleted file mode 100644 index b044d4247..000000000 --- a/tool/kratos-gen-project/templates/all/test/docker-compose.yaml +++ /dev/null @@ -1,40 +0,0 @@ - version: "3.7" - services: - db: - image: mysql:5.6 - ports: - - 13306:3306 - environment: - - MYSQL_ROOT_PASSWORD=root - - TZ=Asia/Shanghai - volumes: - - .:/docker-entrypoint-initdb.d - command: [ - '--character-set-server=utf8', - '--collation-server=utf8_unicode_ci' - ] - healthcheck: - test: ["CMD", "mysqladmin" ,"ping", "--protocol=tcp"] - timeout: 20s - interval: 1s - retries: 20 - - redis: - image: redis - ports: - - 16379:6379 - healthcheck: - test: ["CMD", "redis-cli","ping"] - interval: 20s - timeout: 1s - retries: 20 - - memcached: - image: memcached - ports: - - 21211:11211 - healthcheck: - test: ["CMD", "echo", "stats", "|", "nc", "127.0.0.1", "11211"] - interval: 20s - timeout: 1s - retries: 20 diff --git a/tool/kratos-gen-project/templates/all/test/grpc.toml b/tool/kratos-gen-project/templates/all/test/grpc.toml deleted file mode 100644 index 40339cecc..000000000 --- a/tool/kratos-gen-project/templates/all/test/grpc.toml +++ /dev/null @@ -1,3 +0,0 @@ -[Server] - addr = "0.0.0.0:9000" - timeout = "1s" diff --git a/tool/kratos-gen-project/templates/all/test/http.toml b/tool/kratos-gen-project/templates/all/test/http.toml deleted file mode 100644 index 951a03197..000000000 --- a/tool/kratos-gen-project/templates/all/test/http.toml +++ /dev/null @@ -1,3 +0,0 @@ -[Server] - addr = "0.0.0.0:8000" - timeout = "1s" diff --git a/tool/kratos-gen-project/templates/all/test/memcache.toml b/tool/kratos-gen-project/templates/all/test/memcache.toml deleted file mode 100644 index ad2ce9e09..000000000 --- a/tool/kratos-gen-project/templates/all/test/memcache.toml +++ /dev/null @@ -1,10 +0,0 @@ -[Client] - name = "demo" - proto = "tcp" - addr = "127.0.0.1:21211" - active = 50 - idle = 10 - dialTimeout = "100ms" - readTimeout = "200ms" - writeTimeout = "300ms" - idleTimeout = "80s" diff --git a/tool/kratos-gen-project/templates/all/test/redis.toml b/tool/kratos-gen-project/templates/all/test/redis.toml deleted file mode 100644 index 371f243da..000000000 --- a/tool/kratos-gen-project/templates/all/test/redis.toml +++ /dev/null @@ -1,10 +0,0 @@ -[Client] - name = "demo" - proto = "tcp" - addr = "127.0.0.1:16379" - idle = 10 - active = 10 - dialTimeout = "1s" - readTimeout = "1s" - writeTimeout = "1s" - idleTimeout = "10s" diff --git a/tool/kratos-gen-project/templates/grpc/CHANGELOG.md b/tool/kratos-gen-project/templates/grpc/CHANGELOG.md deleted file mode 100644 index c39acc0e2..000000000 --- a/tool/kratos-gen-project/templates/grpc/CHANGELOG.md +++ /dev/null @@ -1,4 +0,0 @@ -## Demo - -### v1.0.0 -1. 上线功能xxx diff --git a/tool/kratos-gen-project/templates/grpc/OWNERS b/tool/kratos-gen-project/templates/grpc/OWNERS deleted file mode 100644 index c1e28c554..000000000 --- a/tool/kratos-gen-project/templates/grpc/OWNERS +++ /dev/null @@ -1,2 +0,0 @@ -# Author -# Reviewer diff --git a/tool/kratos-gen-project/templates/grpc/README.md b/tool/kratos-gen-project/templates/grpc/README.md deleted file mode 100644 index e43f93fc3..000000000 --- a/tool/kratos-gen-project/templates/grpc/README.md +++ /dev/null @@ -1,4 +0,0 @@ -# Demo - -## 项目简介 -1. diff --git a/tool/kratos-gen-project/templates/grpc/api/api.proto b/tool/kratos-gen-project/templates/grpc/api/api.proto deleted file mode 100644 index 0ba83f9f5..000000000 --- a/tool/kratos-gen-project/templates/grpc/api/api.proto +++ /dev/null @@ -1,34 +0,0 @@ -// 定义项目 API 的 proto 文件 可以同时描述 gRPC 和 HTTP API -// protobuf 文件参考: -// - https://developers.google.com/protocol-buffers/ -syntax = "proto3"; - -import "github.com/gogo/protobuf/gogoproto/gogo.proto"; -import "google/protobuf/empty.proto"; -import "google/api/annotations.proto"; - -// package 命名使用 {appid}.{version} 的方式, version 形如 v1, v2 .. -package demo.service.v1; - -// NOTE: 最后请删除这些无用的注释 (゜-゜)つロ - -option go_package = "api"; -option (gogoproto.goproto_getters_all) = false; - -service Demo { - rpc Ping(.google.protobuf.Empty) returns (.google.protobuf.Empty); - rpc SayHello(HelloReq) returns (.google.protobuf.Empty); - rpc SayHelloURL(HelloReq) returns (HelloResp) { - option (google.api.http) = { - get: "/kratos-demo/say_hello" - }; - }; -} - -message HelloReq { - string name = 1 [(gogoproto.moretags) = 'form:"name" validate:"required"']; -} - -message HelloResp { - string Content = 1 [(gogoproto.jsontag) = 'content']; -} diff --git a/tool/kratos-gen-project/templates/grpc/api/client.go.tmpl b/tool/kratos-gen-project/templates/grpc/api/client.go.tmpl deleted file mode 100644 index 854cc383a..000000000 --- a/tool/kratos-gen-project/templates/grpc/api/client.go.tmpl +++ /dev/null @@ -1,25 +0,0 @@ -package api -import ( - "context" - "fmt" - - "github.com/go-kratos/kratos/pkg/net/rpc/warden" - - "google.golang.org/grpc" -) - -// AppID . -const AppID = "TODO: ADD APP ID" - -// NewClient new grpc client -func NewClient(cfg *warden.ClientConfig, opts ...grpc.DialOption) (DemoClient, error) { - client := warden.NewClient(cfg, opts...) - cc, err := client.Dial(context.Background(), fmt.Sprintf("discovery://default/%s", AppID)) - if err != nil { - return nil, err - } - return NewDemoClient(cc), nil -} - -// 生成 gRPC 代码 -//go:generate kratos tool protoc --grpc api.proto diff --git a/tool/kratos-gen-project/templates/grpc/cmd/main.go.tmpl b/tool/kratos-gen-project/templates/grpc/cmd/main.go.tmpl deleted file mode 100644 index de1aefec2..000000000 --- a/tool/kratos-gen-project/templates/grpc/cmd/main.go.tmpl +++ /dev/null @@ -1,41 +0,0 @@ -package main - -import ( - "flag" - "os" - "os/signal" - "syscall" - "time" - - "{{.ModPrefix}}{{.Name}}/internal/di" - "github.com/go-kratos/kratos/pkg/conf/paladin" - "github.com/go-kratos/kratos/pkg/log" -) - -func main() { - flag.Parse() - log.Init(nil) // debug flag: log.dir={path} - defer log.Close() - log.Info("{{.Name}} start") - paladin.Init() - _, closeFunc, err := di.InitApp() - if err != nil { - panic(err) - } - c := make(chan os.Signal, 1) - signal.Notify(c, syscall.SIGHUP, syscall.SIGQUIT, syscall.SIGTERM, syscall.SIGINT) - for { - s := <-c - log.Info("get a signal %s", s.String()) - switch s { - case syscall.SIGQUIT, syscall.SIGTERM, syscall.SIGINT: - closeFunc() - log.Info("{{.Name}} exit") - time.Sleep(time.Second) - return - case syscall.SIGHUP: - default: - return - } - } -} diff --git a/tool/kratos-gen-project/templates/grpc/configs/application.toml b/tool/kratos-gen-project/templates/grpc/configs/application.toml deleted file mode 100644 index a42ca6e63..000000000 --- a/tool/kratos-gen-project/templates/grpc/configs/application.toml +++ /dev/null @@ -1,3 +0,0 @@ - -# This is a TOML document. Boom~ -demoExpire = "24h" diff --git a/tool/kratos-gen-project/templates/grpc/configs/db.toml b/tool/kratos-gen-project/templates/grpc/configs/db.toml deleted file mode 100644 index 840bd2e78..000000000 --- a/tool/kratos-gen-project/templates/grpc/configs/db.toml +++ /dev/null @@ -1,10 +0,0 @@ -[Client] - addr = "127.0.0.1:3306" - dsn = "{user}:{password}@tcp(127.0.0.1:3306)/{database}?timeout=1s&readTimeout=1s&writeTimeout=1s&parseTime=true&loc=Local&charset=utf8mb4,utf8" - readDSN = ["{user}:{password}@tcp(127.0.0.2:3306)/{database}?timeout=1s&readTimeout=1s&writeTimeout=1s&parseTime=true&loc=Local&charset=utf8mb4,utf8","{user}:{password}@tcp(127.0.0.3:3306)/{database}?timeout=1s&readTimeout=1s&writeTimeout=1s&parseTime=true&loc=Local&charset=utf8,utf8mb4"] - active = 20 - idle = 10 - idleTimeout ="4h" - queryTimeout = "200ms" - execTimeout = "300ms" - tranTimeout = "400ms" diff --git a/tool/kratos-gen-project/templates/grpc/configs/grpc.toml b/tool/kratos-gen-project/templates/grpc/configs/grpc.toml deleted file mode 100644 index 40339cecc..000000000 --- a/tool/kratos-gen-project/templates/grpc/configs/grpc.toml +++ /dev/null @@ -1,3 +0,0 @@ -[Server] - addr = "0.0.0.0:9000" - timeout = "1s" diff --git a/tool/kratos-gen-project/templates/grpc/configs/memcache.toml b/tool/kratos-gen-project/templates/grpc/configs/memcache.toml deleted file mode 100644 index 0f2b900d3..000000000 --- a/tool/kratos-gen-project/templates/grpc/configs/memcache.toml +++ /dev/null @@ -1,10 +0,0 @@ -[Client] - name = "demo" - proto = "tcp" - addr = "127.0.0.1:11211" - active = 50 - idle = 10 - dialTimeout = "100ms" - readTimeout = "200ms" - writeTimeout = "300ms" - idleTimeout = "80s" diff --git a/tool/kratos-gen-project/templates/grpc/configs/redis.toml b/tool/kratos-gen-project/templates/grpc/configs/redis.toml deleted file mode 100644 index d07950de0..000000000 --- a/tool/kratos-gen-project/templates/grpc/configs/redis.toml +++ /dev/null @@ -1,10 +0,0 @@ -[Client] - name = "demo" - proto = "tcp" - addr = "127.0.0.1:6379" - idle = 10 - active = 10 - dialTimeout = "1s" - readTimeout = "1s" - writeTimeout = "1s" - idleTimeout = "10s" diff --git a/tool/kratos-gen-project/templates/grpc/go.mod.tmpl b/tool/kratos-gen-project/templates/grpc/go.mod.tmpl deleted file mode 100644 index fc8719d34..000000000 --- a/tool/kratos-gen-project/templates/grpc/go.mod.tmpl +++ /dev/null @@ -1,12 +0,0 @@ -module {{.Name}} - -go 1.13 - -require ( - github.com/go-kratos/kratos master - github.com/gogo/protobuf v1.2.1 - github.com/golang/protobuf v1.3.2 - golang.org/x/net v0.0.0-20190628185345-da137c7871d7 - google.golang.org/grpc v1.28.1 -) - diff --git a/tool/kratos-gen-project/templates/grpc/internal/dao/dao.go.tmpl b/tool/kratos-gen-project/templates/grpc/internal/dao/dao.go.tmpl deleted file mode 100644 index 81ea0b540..000000000 --- a/tool/kratos-gen-project/templates/grpc/internal/dao/dao.go.tmpl +++ /dev/null @@ -1,69 +0,0 @@ -package dao - -import ( - "context" - "time" - - "{{.ModPrefix}}{{.Name}}/internal/model" - "github.com/go-kratos/kratos/pkg/cache/memcache" - "github.com/go-kratos/kratos/pkg/cache/redis" - "github.com/go-kratos/kratos/pkg/conf/paladin" - "github.com/go-kratos/kratos/pkg/database/sql" - "github.com/go-kratos/kratos/pkg/sync/pipeline/fanout" - xtime "github.com/go-kratos/kratos/pkg/time" - - "github.com/google/wire" -) - -var Provider = wire.NewSet(New, NewDB, NewRedis, NewMC) - -//go:generate kratos tool genbts -// Dao dao interface -type Dao interface { - Close() - Ping(ctx context.Context) (err error) - // bts: -nullcache=&model.Article{ID:-1} -check_null_code=$!=nil&&$.ID==-1 - RawArticle(c context.Context, id int64) (*model.Article, error) -} - -// dao dao. -type dao struct { - db *sql.DB - redis *redis.Redis - mc *memcache.Memcache - cache *fanout.Fanout - demoExpire int32 -} - -// New new a dao and return. -func New(r *redis.Redis, mc *memcache.Memcache, db *sql.DB) (d Dao, cf func(), err error) { - return newDao(r, mc, db) -} - -func newDao(r *redis.Redis, mc *memcache.Memcache, db *sql.DB) (d *dao, cf func(), err error) { - var cfg struct{ - DemoExpire xtime.Duration - } - if err = paladin.Get("application.toml").UnmarshalTOML(&cfg); err != nil { - return - } - d = &dao{ - db: db, - redis: r, - mc: mc, - cache: fanout.New("cache"), - demoExpire: int32(time.Duration(cfg.DemoExpire) / time.Second), - } - cf = d.Close - return -} - -// Close close the resource. -func (d *dao) Close() { - d.cache.Close() -} - -// Ping ping the resource. -func (d *dao) Ping(ctx context.Context) (err error) { - return nil -} diff --git a/tool/kratos-gen-project/templates/grpc/internal/dao/dao_test.go.tmpl b/tool/kratos-gen-project/templates/grpc/internal/dao/dao_test.go.tmpl deleted file mode 100644 index 90849e012..000000000 --- a/tool/kratos-gen-project/templates/grpc/internal/dao/dao_test.go.tmpl +++ /dev/null @@ -1,40 +0,0 @@ -package dao - -import ( - "context" - "flag" - "os" - "testing" - - "github.com/go-kratos/kratos/pkg/conf/paladin" - "github.com/go-kratos/kratos/pkg/testing/lich" -) - -var d *dao -var ctx = context.Background() - -func TestMain(m *testing.M) { - flag.Set("conf", "../../test") - flag.Set("f", "../../test/docker-compose.yaml") - flag.Parse() - disableLich := os.Getenv("DISABLE_LICH") != "" - if !disableLich { - if err := lich.Setup(); err != nil { - panic(err) - } - } - var err error - if err = paladin.Init(); err != nil { - panic(err) - } - var cf func() - if d, cf, err = newTestDao();err != nil { - panic(err) - } - ret := m.Run() - cf() - if !disableLich { - _ = lich.Teardown() - } - os.Exit(ret) -} diff --git a/tool/kratos-gen-project/templates/grpc/internal/dao/db.go.tmpl b/tool/kratos-gen-project/templates/grpc/internal/dao/db.go.tmpl deleted file mode 100644 index b3bde3064..000000000 --- a/tool/kratos-gen-project/templates/grpc/internal/dao/db.go.tmpl +++ /dev/null @@ -1,30 +0,0 @@ -package dao - -import ( - "context" - - "{{.ModPrefix}}{{.Name}}/internal/model" - "github.com/go-kratos/kratos/pkg/conf/paladin" - "github.com/go-kratos/kratos/pkg/database/sql" -) - -func NewDB() (db *sql.DB, cf func(), err error) { - var ( - cfg sql.Config - ct paladin.TOML - ) - if err = paladin.Get("db.toml").Unmarshal(&ct); err != nil { - return - } - if err = ct.Get("Client").UnmarshalTOML(&cfg); err != nil { - return - } - db = sql.NewMySQL(&cfg) - cf = func() {db.Close()} - return -} - -func (d *dao) RawArticle(ctx context.Context, id int64) (art *model.Article, err error) { - // get data from db - return -} diff --git a/tool/kratos-gen-project/templates/grpc/internal/dao/mc.go.tmpl b/tool/kratos-gen-project/templates/grpc/internal/dao/mc.go.tmpl deleted file mode 100644 index 44cfdf13b..000000000 --- a/tool/kratos-gen-project/templates/grpc/internal/dao/mc.go.tmpl +++ /dev/null @@ -1,48 +0,0 @@ -package dao - -import ( - "context" - "fmt" - - "{{.ModPrefix}}{{.Name}}/internal/model" - "github.com/go-kratos/kratos/pkg/cache/memcache" - "github.com/go-kratos/kratos/pkg/conf/paladin" - "github.com/go-kratos/kratos/pkg/log" -) - -//go:generate kratos tool genmc -type _mc interface { - // mc: -key=keyArt -type=get - CacheArticle(c context.Context, id int64) (*model.Article, error) - // mc: -key=keyArt -expire=d.demoExpire - AddCacheArticle(c context.Context, id int64, art *model.Article) (err error) - // mc: -key=keyArt - DeleteArticleCache(c context.Context, id int64) (err error) -} - -func NewMC() (mc *memcache.Memcache, cf func(), err error) { - var ( - cfg memcache.Config - ct paladin.TOML - ) - if err = paladin.Get("memcache.toml").Unmarshal(&ct); err != nil { - return - } - if err = ct.Get("Client").UnmarshalTOML(&cfg); err != nil { - return - } - mc = memcache.New(&cfg) - cf = func() {mc.Close()} - return -} - -func (d *dao) PingMC(ctx context.Context) (err error) { - if err = d.mc.Set(ctx, &memcache.Item{Key: "ping", Value: []byte("pong"), Expiration: 0}); err != nil { - log.Error("conn.Set(PING) error(%v)", err) - } - return -} - -func keyArt(id int64) string { - return fmt.Sprintf("art_%d", id) -} diff --git a/tool/kratos-gen-project/templates/grpc/internal/dao/redis.go.tmpl b/tool/kratos-gen-project/templates/grpc/internal/dao/redis.go.tmpl deleted file mode 100644 index e3c962eee..000000000 --- a/tool/kratos-gen-project/templates/grpc/internal/dao/redis.go.tmpl +++ /dev/null @@ -1,32 +0,0 @@ -package dao - -import ( - "context" - - "github.com/go-kratos/kratos/pkg/cache/redis" - "github.com/go-kratos/kratos/pkg/conf/paladin" - "github.com/go-kratos/kratos/pkg/log" -) - -func NewRedis() (r *redis.Redis, cf func(), err error) { - var ( - cfg redis.Config - ct paladin.Map - ) - if err = paladin.Get("redis.toml").Unmarshal(&ct); err != nil { - return - } - if err = ct.Get("Client").UnmarshalTOML(&cfg); err != nil { - return - } - r = redis.NewRedis(&cfg) - cf = func(){r.Close()} - return -} - -func (d *dao) PingRedis(ctx context.Context) (err error) { - if _, err = d.redis.Do(ctx, "SET", "ping", "pong"); err != nil { - log.Error("conn.Set(PING) error(%v)", err) - } - return -} \ No newline at end of file diff --git a/tool/kratos-gen-project/templates/grpc/internal/dao/wire.go.tmpl b/tool/kratos-gen-project/templates/grpc/internal/dao/wire.go.tmpl deleted file mode 100644 index a00b2d39a..000000000 --- a/tool/kratos-gen-project/templates/grpc/internal/dao/wire.go.tmpl +++ /dev/null @@ -1,13 +0,0 @@ -// +build wireinject -// The build tag makes sure the stub is not built in the final build. - -package dao - -import ( - "github.com/google/wire" -) - -//go:generate kratos tool wire -func newTestDao() (*dao, func(), error) { - panic(wire.Build(newDao, NewDB, NewRedis, NewMC)) -} diff --git a/tool/kratos-gen-project/templates/grpc/internal/di/app.go.tmpl b/tool/kratos-gen-project/templates/grpc/internal/di/app.go.tmpl deleted file mode 100644 index f660e4f15..000000000 --- a/tool/kratos-gen-project/templates/grpc/internal/di/app.go.tmpl +++ /dev/null @@ -1,32 +0,0 @@ -package di - -import ( - "context" - "time" - - "{{.ModPrefix}}{{.Name}}/internal/service" - - "github.com/go-kratos/kratos/pkg/log" - "github.com/go-kratos/kratos/pkg/net/rpc/warden" -) - -//go:generate kratos tool wire -type App struct { - svc *service.Service - grpc *warden.Server -} - -func NewApp(svc *service.Service, g *warden.Server) (app *App, closeFunc func(), err error){ - app = &App{ - svc: svc, - grpc: g, - } - closeFunc = func() { - ctx, cancel := context.WithTimeout(context.Background(), 35*time.Second) - if err := g.Shutdown(ctx); err != nil { - log.Error("grpcSrv.Shutdown error(%v)", err) - } - cancel() - } - return -} diff --git a/tool/kratos-gen-project/templates/grpc/internal/di/wire.go.tmpl b/tool/kratos-gen-project/templates/grpc/internal/di/wire.go.tmpl deleted file mode 100644 index bbed6327c..000000000 --- a/tool/kratos-gen-project/templates/grpc/internal/di/wire.go.tmpl +++ /dev/null @@ -1,17 +0,0 @@ -// +build wireinject -// The build tag makes sure the stub is not built in the final build. - -package di - -import ( - "{{.ModPrefix}}{{.Name}}/internal/dao" - "{{.ModPrefix}}{{.Name}}/internal/service" - "{{.ModPrefix}}{{.Name}}/internal/server/grpc" - - "github.com/google/wire" -) - -//go:generate kratos t wire -func InitApp() (*App, func(), error) { - panic(wire.Build(dao.Provider, service.Provider, grpc.New, NewApp)) -} diff --git a/tool/kratos-gen-project/templates/grpc/internal/model/model.go.tmpl b/tool/kratos-gen-project/templates/grpc/internal/model/model.go.tmpl deleted file mode 100644 index b3fcf7985..000000000 --- a/tool/kratos-gen-project/templates/grpc/internal/model/model.go.tmpl +++ /dev/null @@ -1,12 +0,0 @@ -package model - -// Kratos hello kratos. -type Kratos struct { - Hello string -} - -type Article struct { - ID int64 - Content string - Author string -} \ No newline at end of file diff --git a/tool/kratos-gen-project/templates/grpc/internal/server/grpc/server.go.tmpl b/tool/kratos-gen-project/templates/grpc/internal/server/grpc/server.go.tmpl deleted file mode 100644 index 4db767635..000000000 --- a/tool/kratos-gen-project/templates/grpc/internal/server/grpc/server.go.tmpl +++ /dev/null @@ -1,26 +0,0 @@ -package grpc - -import ( - pb "{{.ModPrefix}}{{.Name}}/api" - - "github.com/go-kratos/kratos/pkg/conf/paladin" - "github.com/go-kratos/kratos/pkg/net/rpc/warden" -) - -// New new a grpc server. -func New(svc pb.DemoServer) (ws *warden.Server, err error) { - var ( - cfg warden.ServerConfig - ct paladin.TOML - ) - if err = paladin.Get("grpc.toml").Unmarshal(&ct); err != nil { - return - } - if err = ct.Get("Server").UnmarshalTOML(&cfg); err != nil { - return - } - ws = warden.NewServer(&cfg) - pb.RegisterDemoServer(ws.Server(), svc) - ws, err = ws.Start() - return -} diff --git a/tool/kratos-gen-project/templates/grpc/internal/service/service.go.tmpl b/tool/kratos-gen-project/templates/grpc/internal/service/service.go.tmpl deleted file mode 100644 index f3750cc69..000000000 --- a/tool/kratos-gen-project/templates/grpc/internal/service/service.go.tmpl +++ /dev/null @@ -1,57 +0,0 @@ -package service - -import ( - "context" - "fmt" - - pb "{{.ModPrefix}}{{.Name}}/api" - "{{.ModPrefix}}{{.Name}}/internal/dao" - "github.com/go-kratos/kratos/pkg/conf/paladin" - - "github.com/golang/protobuf/ptypes/empty" - "github.com/google/wire" -) - -var Provider = wire.NewSet(New, wire.Bind(new(pb.DemoServer), new(*Service))) - -// Service service. -type Service struct { - ac *paladin.Map - dao dao.Dao -} - -// New new a service and return. -func New(d dao.Dao) (s *Service, cf func(), err error) { - s = &Service{ - ac: &paladin.TOML{}, - dao: d, - } - cf = s.Close - err = paladin.Watch("application.toml", s.ac) - return -} - -// SayHello grpc demo func. -func (s *Service) SayHello(ctx context.Context, req *pb.HelloReq) (reply *empty.Empty, err error) { - reply = new(empty.Empty) - fmt.Printf("hello %s", req.Name) - return -} - -// SayHelloURL bm demo func. -func (s *Service) SayHelloURL(ctx context.Context, req *pb.HelloReq) (reply *pb.HelloResp, err error) { - reply = &pb.HelloResp{ - Content: "hello " + req.Name, - } - fmt.Printf("hello url %s", req.Name) - return -} - -// Ping ping the resource. -func (s *Service) Ping(ctx context.Context, e *empty.Empty) (*empty.Empty, error) { - return &empty.Empty{}, s.dao.Ping(ctx) -} - -// Close close the resource. -func (s *Service) Close() { -} diff --git a/tool/kratos-gen-project/templates/grpc/test/0_db.sql b/tool/kratos-gen-project/templates/grpc/test/0_db.sql deleted file mode 100644 index e2cbccc19..000000000 --- a/tool/kratos-gen-project/templates/grpc/test/0_db.sql +++ /dev/null @@ -1,11 +0,0 @@ -create database kratos_demo; -use kratos_demo; - -CREATE TABLE `articles` ( - `id` int(11) UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '主键ID', - `title` varchar(64) NOT NULL COMMENT '名称', - `mtime` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '最后修改时间', - `ctime` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', - PRIMARY KEY (`id`), - KEY `ix_mtime` (`mtime`) -) COMMENT='文章表'; diff --git a/tool/kratos-gen-project/templates/grpc/test/1_data.sql b/tool/kratos-gen-project/templates/grpc/test/1_data.sql deleted file mode 100644 index c7cb57167..000000000 --- a/tool/kratos-gen-project/templates/grpc/test/1_data.sql +++ /dev/null @@ -1,3 +0,0 @@ -use kratos_demo; - -INSERT INTO articles(`id`, `title`) VALUES (1, 'title'); diff --git a/tool/kratos-gen-project/templates/grpc/test/application.toml b/tool/kratos-gen-project/templates/grpc/test/application.toml deleted file mode 100644 index a42ca6e63..000000000 --- a/tool/kratos-gen-project/templates/grpc/test/application.toml +++ /dev/null @@ -1,3 +0,0 @@ - -# This is a TOML document. Boom~ -demoExpire = "24h" diff --git a/tool/kratos-gen-project/templates/grpc/test/db.toml b/tool/kratos-gen-project/templates/grpc/test/db.toml deleted file mode 100644 index 9c7bd8618..000000000 --- a/tool/kratos-gen-project/templates/grpc/test/db.toml +++ /dev/null @@ -1,8 +0,0 @@ -[Client] - dsn = "root:root@tcp(127.0.0.1:13306)/kratos_demo?timeout=1s&readTimeout=1s&writeTimeout=1s&parseTime=true&loc=Local&charset=utf8mb4,utf8" - active = 20 - idle = 10 - idleTimeout ="4h" - queryTimeout = "200ms" - execTimeout = "300ms" - tranTimeout = "400ms" diff --git a/tool/kratos-gen-project/templates/grpc/test/docker-compose.yaml b/tool/kratos-gen-project/templates/grpc/test/docker-compose.yaml deleted file mode 100644 index b044d4247..000000000 --- a/tool/kratos-gen-project/templates/grpc/test/docker-compose.yaml +++ /dev/null @@ -1,40 +0,0 @@ - version: "3.7" - services: - db: - image: mysql:5.6 - ports: - - 13306:3306 - environment: - - MYSQL_ROOT_PASSWORD=root - - TZ=Asia/Shanghai - volumes: - - .:/docker-entrypoint-initdb.d - command: [ - '--character-set-server=utf8', - '--collation-server=utf8_unicode_ci' - ] - healthcheck: - test: ["CMD", "mysqladmin" ,"ping", "--protocol=tcp"] - timeout: 20s - interval: 1s - retries: 20 - - redis: - image: redis - ports: - - 16379:6379 - healthcheck: - test: ["CMD", "redis-cli","ping"] - interval: 20s - timeout: 1s - retries: 20 - - memcached: - image: memcached - ports: - - 21211:11211 - healthcheck: - test: ["CMD", "echo", "stats", "|", "nc", "127.0.0.1", "11211"] - interval: 20s - timeout: 1s - retries: 20 diff --git a/tool/kratos-gen-project/templates/grpc/test/grpc.toml b/tool/kratos-gen-project/templates/grpc/test/grpc.toml deleted file mode 100644 index 40339cecc..000000000 --- a/tool/kratos-gen-project/templates/grpc/test/grpc.toml +++ /dev/null @@ -1,3 +0,0 @@ -[Server] - addr = "0.0.0.0:9000" - timeout = "1s" diff --git a/tool/kratos-gen-project/templates/grpc/test/memcache.toml b/tool/kratos-gen-project/templates/grpc/test/memcache.toml deleted file mode 100644 index ad2ce9e09..000000000 --- a/tool/kratos-gen-project/templates/grpc/test/memcache.toml +++ /dev/null @@ -1,10 +0,0 @@ -[Client] - name = "demo" - proto = "tcp" - addr = "127.0.0.1:21211" - active = 50 - idle = 10 - dialTimeout = "100ms" - readTimeout = "200ms" - writeTimeout = "300ms" - idleTimeout = "80s" diff --git a/tool/kratos-gen-project/templates/grpc/test/redis.toml b/tool/kratos-gen-project/templates/grpc/test/redis.toml deleted file mode 100644 index 371f243da..000000000 --- a/tool/kratos-gen-project/templates/grpc/test/redis.toml +++ /dev/null @@ -1,10 +0,0 @@ -[Client] - name = "demo" - proto = "tcp" - addr = "127.0.0.1:16379" - idle = 10 - active = 10 - dialTimeout = "1s" - readTimeout = "1s" - writeTimeout = "1s" - idleTimeout = "10s" diff --git a/tool/kratos-gen-project/templates/http/CHANGELOG.md b/tool/kratos-gen-project/templates/http/CHANGELOG.md deleted file mode 100644 index c39acc0e2..000000000 --- a/tool/kratos-gen-project/templates/http/CHANGELOG.md +++ /dev/null @@ -1,4 +0,0 @@ -## Demo - -### v1.0.0 -1. 上线功能xxx diff --git a/tool/kratos-gen-project/templates/http/OWNERS b/tool/kratos-gen-project/templates/http/OWNERS deleted file mode 100644 index c1e28c554..000000000 --- a/tool/kratos-gen-project/templates/http/OWNERS +++ /dev/null @@ -1,2 +0,0 @@ -# Author -# Reviewer diff --git a/tool/kratos-gen-project/templates/http/README.md b/tool/kratos-gen-project/templates/http/README.md deleted file mode 100644 index e43f93fc3..000000000 --- a/tool/kratos-gen-project/templates/http/README.md +++ /dev/null @@ -1,4 +0,0 @@ -# Demo - -## 项目简介 -1. diff --git a/tool/kratos-gen-project/templates/http/api/api.proto b/tool/kratos-gen-project/templates/http/api/api.proto deleted file mode 100644 index ed1664f5e..000000000 --- a/tool/kratos-gen-project/templates/http/api/api.proto +++ /dev/null @@ -1,34 +0,0 @@ -// 定义项目 API 的 proto 文件 可以同时描述 gRPC 和 HTTP API -// protobuf 文件参考: -// - https://developers.google.com/protocol-buffers/ -syntax = "proto3"; - -import "github.com/gogo/protobuf/gogoproto/gogo.proto"; -import "google/protobuf/empty.proto"; -import "google/api/annotations.proto"; - -// package 命名使用 {appid}.{version} 的方式, version 形如 v1, v2 .. -package demo.service.v1; - -// NOTE: 最后请删除这些无用的注释 (゜-゜)つロ - -option go_package = "api"; -option (gogoproto.goproto_getters_all) = false; - -service Demo { - rpc Ping(.google.protobuf.Empty) returns (.google.protobuf.Empty); - rpc SayHello(HelloReq) returns (.google.protobuf.Empty); - rpc SayHelloURL(HelloReq) returns (HelloResp) { - option (google.api.http) = { - get: "/demo/say_hello" - }; - }; -} - -message HelloReq { - string name = 1 [(gogoproto.moretags) = 'form:"name" validate:"required"']; -} - -message HelloResp { - string Content = 1 [(gogoproto.jsontag) = 'content']; -} diff --git a/tool/kratos-gen-project/templates/http/api/client.go.tmpl b/tool/kratos-gen-project/templates/http/api/client.go.tmpl deleted file mode 100644 index f3a3b2083..000000000 --- a/tool/kratos-gen-project/templates/http/api/client.go.tmpl +++ /dev/null @@ -1,25 +0,0 @@ -package api -import ( - "context" - "fmt" - - "github.com/go-kratos/kratos/pkg/net/rpc/warden" - - "google.golang.org/grpc" -) - -// AppID . -const AppID = "TODO: ADD APP ID" - -// NewClient new grpc client -func NewClient(cfg *warden.ClientConfig, opts ...grpc.DialOption) (DemoClient, error) { - client := warden.NewClient(cfg, opts...) - cc, err := client.Dial(context.Background(), fmt.Sprintf("discovery://default/%s", AppID)) - if err != nil { - return nil, err - } - return NewDemoClient(cc), nil -} - -// 生成 gRPC 代码 -//go:generate kratos tool protoc --grpc --bm api.proto diff --git a/tool/kratos-gen-project/templates/http/cmd/main.go.tmpl b/tool/kratos-gen-project/templates/http/cmd/main.go.tmpl deleted file mode 100644 index de1aefec2..000000000 --- a/tool/kratos-gen-project/templates/http/cmd/main.go.tmpl +++ /dev/null @@ -1,41 +0,0 @@ -package main - -import ( - "flag" - "os" - "os/signal" - "syscall" - "time" - - "{{.ModPrefix}}{{.Name}}/internal/di" - "github.com/go-kratos/kratos/pkg/conf/paladin" - "github.com/go-kratos/kratos/pkg/log" -) - -func main() { - flag.Parse() - log.Init(nil) // debug flag: log.dir={path} - defer log.Close() - log.Info("{{.Name}} start") - paladin.Init() - _, closeFunc, err := di.InitApp() - if err != nil { - panic(err) - } - c := make(chan os.Signal, 1) - signal.Notify(c, syscall.SIGHUP, syscall.SIGQUIT, syscall.SIGTERM, syscall.SIGINT) - for { - s := <-c - log.Info("get a signal %s", s.String()) - switch s { - case syscall.SIGQUIT, syscall.SIGTERM, syscall.SIGINT: - closeFunc() - log.Info("{{.Name}} exit") - time.Sleep(time.Second) - return - case syscall.SIGHUP: - default: - return - } - } -} diff --git a/tool/kratos-gen-project/templates/http/configs/application.toml b/tool/kratos-gen-project/templates/http/configs/application.toml deleted file mode 100644 index a42ca6e63..000000000 --- a/tool/kratos-gen-project/templates/http/configs/application.toml +++ /dev/null @@ -1,3 +0,0 @@ - -# This is a TOML document. Boom~ -demoExpire = "24h" diff --git a/tool/kratos-gen-project/templates/http/configs/db.toml b/tool/kratos-gen-project/templates/http/configs/db.toml deleted file mode 100644 index 840bd2e78..000000000 --- a/tool/kratos-gen-project/templates/http/configs/db.toml +++ /dev/null @@ -1,10 +0,0 @@ -[Client] - addr = "127.0.0.1:3306" - dsn = "{user}:{password}@tcp(127.0.0.1:3306)/{database}?timeout=1s&readTimeout=1s&writeTimeout=1s&parseTime=true&loc=Local&charset=utf8mb4,utf8" - readDSN = ["{user}:{password}@tcp(127.0.0.2:3306)/{database}?timeout=1s&readTimeout=1s&writeTimeout=1s&parseTime=true&loc=Local&charset=utf8mb4,utf8","{user}:{password}@tcp(127.0.0.3:3306)/{database}?timeout=1s&readTimeout=1s&writeTimeout=1s&parseTime=true&loc=Local&charset=utf8,utf8mb4"] - active = 20 - idle = 10 - idleTimeout ="4h" - queryTimeout = "200ms" - execTimeout = "300ms" - tranTimeout = "400ms" diff --git a/tool/kratos-gen-project/templates/http/configs/http.toml b/tool/kratos-gen-project/templates/http/configs/http.toml deleted file mode 100644 index 951a03197..000000000 --- a/tool/kratos-gen-project/templates/http/configs/http.toml +++ /dev/null @@ -1,3 +0,0 @@ -[Server] - addr = "0.0.0.0:8000" - timeout = "1s" diff --git a/tool/kratos-gen-project/templates/http/configs/memcache.toml b/tool/kratos-gen-project/templates/http/configs/memcache.toml deleted file mode 100644 index 0f2b900d3..000000000 --- a/tool/kratos-gen-project/templates/http/configs/memcache.toml +++ /dev/null @@ -1,10 +0,0 @@ -[Client] - name = "demo" - proto = "tcp" - addr = "127.0.0.1:11211" - active = 50 - idle = 10 - dialTimeout = "100ms" - readTimeout = "200ms" - writeTimeout = "300ms" - idleTimeout = "80s" diff --git a/tool/kratos-gen-project/templates/http/configs/redis.toml b/tool/kratos-gen-project/templates/http/configs/redis.toml deleted file mode 100644 index d07950de0..000000000 --- a/tool/kratos-gen-project/templates/http/configs/redis.toml +++ /dev/null @@ -1,10 +0,0 @@ -[Client] - name = "demo" - proto = "tcp" - addr = "127.0.0.1:6379" - idle = 10 - active = 10 - dialTimeout = "1s" - readTimeout = "1s" - writeTimeout = "1s" - idleTimeout = "10s" diff --git a/tool/kratos-gen-project/templates/http/go.mod.tmpl b/tool/kratos-gen-project/templates/http/go.mod.tmpl deleted file mode 100644 index fc8719d34..000000000 --- a/tool/kratos-gen-project/templates/http/go.mod.tmpl +++ /dev/null @@ -1,12 +0,0 @@ -module {{.Name}} - -go 1.13 - -require ( - github.com/go-kratos/kratos master - github.com/gogo/protobuf v1.2.1 - github.com/golang/protobuf v1.3.2 - golang.org/x/net v0.0.0-20190628185345-da137c7871d7 - google.golang.org/grpc v1.28.1 -) - diff --git a/tool/kratos-gen-project/templates/http/internal/dao/dao.go.tmpl b/tool/kratos-gen-project/templates/http/internal/dao/dao.go.tmpl deleted file mode 100644 index 81ea0b540..000000000 --- a/tool/kratos-gen-project/templates/http/internal/dao/dao.go.tmpl +++ /dev/null @@ -1,69 +0,0 @@ -package dao - -import ( - "context" - "time" - - "{{.ModPrefix}}{{.Name}}/internal/model" - "github.com/go-kratos/kratos/pkg/cache/memcache" - "github.com/go-kratos/kratos/pkg/cache/redis" - "github.com/go-kratos/kratos/pkg/conf/paladin" - "github.com/go-kratos/kratos/pkg/database/sql" - "github.com/go-kratos/kratos/pkg/sync/pipeline/fanout" - xtime "github.com/go-kratos/kratos/pkg/time" - - "github.com/google/wire" -) - -var Provider = wire.NewSet(New, NewDB, NewRedis, NewMC) - -//go:generate kratos tool genbts -// Dao dao interface -type Dao interface { - Close() - Ping(ctx context.Context) (err error) - // bts: -nullcache=&model.Article{ID:-1} -check_null_code=$!=nil&&$.ID==-1 - RawArticle(c context.Context, id int64) (*model.Article, error) -} - -// dao dao. -type dao struct { - db *sql.DB - redis *redis.Redis - mc *memcache.Memcache - cache *fanout.Fanout - demoExpire int32 -} - -// New new a dao and return. -func New(r *redis.Redis, mc *memcache.Memcache, db *sql.DB) (d Dao, cf func(), err error) { - return newDao(r, mc, db) -} - -func newDao(r *redis.Redis, mc *memcache.Memcache, db *sql.DB) (d *dao, cf func(), err error) { - var cfg struct{ - DemoExpire xtime.Duration - } - if err = paladin.Get("application.toml").UnmarshalTOML(&cfg); err != nil { - return - } - d = &dao{ - db: db, - redis: r, - mc: mc, - cache: fanout.New("cache"), - demoExpire: int32(time.Duration(cfg.DemoExpire) / time.Second), - } - cf = d.Close - return -} - -// Close close the resource. -func (d *dao) Close() { - d.cache.Close() -} - -// Ping ping the resource. -func (d *dao) Ping(ctx context.Context) (err error) { - return nil -} diff --git a/tool/kratos-gen-project/templates/http/internal/dao/dao_test.go.tmpl b/tool/kratos-gen-project/templates/http/internal/dao/dao_test.go.tmpl deleted file mode 100644 index 90849e012..000000000 --- a/tool/kratos-gen-project/templates/http/internal/dao/dao_test.go.tmpl +++ /dev/null @@ -1,40 +0,0 @@ -package dao - -import ( - "context" - "flag" - "os" - "testing" - - "github.com/go-kratos/kratos/pkg/conf/paladin" - "github.com/go-kratos/kratos/pkg/testing/lich" -) - -var d *dao -var ctx = context.Background() - -func TestMain(m *testing.M) { - flag.Set("conf", "../../test") - flag.Set("f", "../../test/docker-compose.yaml") - flag.Parse() - disableLich := os.Getenv("DISABLE_LICH") != "" - if !disableLich { - if err := lich.Setup(); err != nil { - panic(err) - } - } - var err error - if err = paladin.Init(); err != nil { - panic(err) - } - var cf func() - if d, cf, err = newTestDao();err != nil { - panic(err) - } - ret := m.Run() - cf() - if !disableLich { - _ = lich.Teardown() - } - os.Exit(ret) -} diff --git a/tool/kratos-gen-project/templates/http/internal/dao/db.go.tmpl b/tool/kratos-gen-project/templates/http/internal/dao/db.go.tmpl deleted file mode 100644 index b3bde3064..000000000 --- a/tool/kratos-gen-project/templates/http/internal/dao/db.go.tmpl +++ /dev/null @@ -1,30 +0,0 @@ -package dao - -import ( - "context" - - "{{.ModPrefix}}{{.Name}}/internal/model" - "github.com/go-kratos/kratos/pkg/conf/paladin" - "github.com/go-kratos/kratos/pkg/database/sql" -) - -func NewDB() (db *sql.DB, cf func(), err error) { - var ( - cfg sql.Config - ct paladin.TOML - ) - if err = paladin.Get("db.toml").Unmarshal(&ct); err != nil { - return - } - if err = ct.Get("Client").UnmarshalTOML(&cfg); err != nil { - return - } - db = sql.NewMySQL(&cfg) - cf = func() {db.Close()} - return -} - -func (d *dao) RawArticle(ctx context.Context, id int64) (art *model.Article, err error) { - // get data from db - return -} diff --git a/tool/kratos-gen-project/templates/http/internal/dao/mc.go.tmpl b/tool/kratos-gen-project/templates/http/internal/dao/mc.go.tmpl deleted file mode 100644 index 44cfdf13b..000000000 --- a/tool/kratos-gen-project/templates/http/internal/dao/mc.go.tmpl +++ /dev/null @@ -1,48 +0,0 @@ -package dao - -import ( - "context" - "fmt" - - "{{.ModPrefix}}{{.Name}}/internal/model" - "github.com/go-kratos/kratos/pkg/cache/memcache" - "github.com/go-kratos/kratos/pkg/conf/paladin" - "github.com/go-kratos/kratos/pkg/log" -) - -//go:generate kratos tool genmc -type _mc interface { - // mc: -key=keyArt -type=get - CacheArticle(c context.Context, id int64) (*model.Article, error) - // mc: -key=keyArt -expire=d.demoExpire - AddCacheArticle(c context.Context, id int64, art *model.Article) (err error) - // mc: -key=keyArt - DeleteArticleCache(c context.Context, id int64) (err error) -} - -func NewMC() (mc *memcache.Memcache, cf func(), err error) { - var ( - cfg memcache.Config - ct paladin.TOML - ) - if err = paladin.Get("memcache.toml").Unmarshal(&ct); err != nil { - return - } - if err = ct.Get("Client").UnmarshalTOML(&cfg); err != nil { - return - } - mc = memcache.New(&cfg) - cf = func() {mc.Close()} - return -} - -func (d *dao) PingMC(ctx context.Context) (err error) { - if err = d.mc.Set(ctx, &memcache.Item{Key: "ping", Value: []byte("pong"), Expiration: 0}); err != nil { - log.Error("conn.Set(PING) error(%v)", err) - } - return -} - -func keyArt(id int64) string { - return fmt.Sprintf("art_%d", id) -} diff --git a/tool/kratos-gen-project/templates/http/internal/dao/redis.go.tmpl b/tool/kratos-gen-project/templates/http/internal/dao/redis.go.tmpl deleted file mode 100644 index e3c962eee..000000000 --- a/tool/kratos-gen-project/templates/http/internal/dao/redis.go.tmpl +++ /dev/null @@ -1,32 +0,0 @@ -package dao - -import ( - "context" - - "github.com/go-kratos/kratos/pkg/cache/redis" - "github.com/go-kratos/kratos/pkg/conf/paladin" - "github.com/go-kratos/kratos/pkg/log" -) - -func NewRedis() (r *redis.Redis, cf func(), err error) { - var ( - cfg redis.Config - ct paladin.Map - ) - if err = paladin.Get("redis.toml").Unmarshal(&ct); err != nil { - return - } - if err = ct.Get("Client").UnmarshalTOML(&cfg); err != nil { - return - } - r = redis.NewRedis(&cfg) - cf = func(){r.Close()} - return -} - -func (d *dao) PingRedis(ctx context.Context) (err error) { - if _, err = d.redis.Do(ctx, "SET", "ping", "pong"); err != nil { - log.Error("conn.Set(PING) error(%v)", err) - } - return -} \ No newline at end of file diff --git a/tool/kratos-gen-project/templates/http/internal/dao/wire.go.tmpl b/tool/kratos-gen-project/templates/http/internal/dao/wire.go.tmpl deleted file mode 100644 index a00b2d39a..000000000 --- a/tool/kratos-gen-project/templates/http/internal/dao/wire.go.tmpl +++ /dev/null @@ -1,13 +0,0 @@ -// +build wireinject -// The build tag makes sure the stub is not built in the final build. - -package dao - -import ( - "github.com/google/wire" -) - -//go:generate kratos tool wire -func newTestDao() (*dao, func(), error) { - panic(wire.Build(newDao, NewDB, NewRedis, NewMC)) -} diff --git a/tool/kratos-gen-project/templates/http/internal/di/app.go.tmpl b/tool/kratos-gen-project/templates/http/internal/di/app.go.tmpl deleted file mode 100644 index b78ce0d37..000000000 --- a/tool/kratos-gen-project/templates/http/internal/di/app.go.tmpl +++ /dev/null @@ -1,32 +0,0 @@ -package di - -import ( - "context" - "time" - - "{{.ModPrefix}}{{.Name}}/internal/service" - - "github.com/go-kratos/kratos/pkg/log" - bm "github.com/go-kratos/kratos/pkg/net/http/blademaster" -) - -//go:generate kratos tool wire -type App struct { - svc *service.Service - http *bm.Engine -} - -func NewApp(svc *service.Service, h *bm.Engine) (app *App, closeFunc func(), err error){ - app = &App{ - svc: svc, - http: h, - } - closeFunc = func() { - ctx, cancel := context.WithTimeout(context.Background(), 35*time.Second) - if err := h.Shutdown(ctx); err != nil { - log.Error("httpSrv.Shutdown error(%v)", err) - } - cancel() - } - return -} diff --git a/tool/kratos-gen-project/templates/http/internal/di/wire.go.tmpl b/tool/kratos-gen-project/templates/http/internal/di/wire.go.tmpl deleted file mode 100644 index 7533269a0..000000000 --- a/tool/kratos-gen-project/templates/http/internal/di/wire.go.tmpl +++ /dev/null @@ -1,17 +0,0 @@ -// +build wireinject -// The build tag makes sure the stub is not built in the final build. - -package di - -import ( - "{{.ModPrefix}}{{.Name}}/internal/dao" - "{{.ModPrefix}}{{.Name}}/internal/service" - "{{.ModPrefix}}{{.Name}}/internal/server/http" - - "github.com/google/wire" -) - -//go:generate kratos t wire -func InitApp() (*App, func(), error) { - panic(wire.Build(dao.Provider, service.Provider, http.New, NewApp)) -} \ No newline at end of file diff --git a/tool/kratos-gen-project/templates/http/internal/model/model.go.tmpl b/tool/kratos-gen-project/templates/http/internal/model/model.go.tmpl deleted file mode 100644 index b3fcf7985..000000000 --- a/tool/kratos-gen-project/templates/http/internal/model/model.go.tmpl +++ /dev/null @@ -1,12 +0,0 @@ -package model - -// Kratos hello kratos. -type Kratos struct { - Hello string -} - -type Article struct { - ID int64 - Content string - Author string -} \ No newline at end of file diff --git a/tool/kratos-gen-project/templates/http/internal/server/http/server.go.tmpl b/tool/kratos-gen-project/templates/http/internal/server/http/server.go.tmpl deleted file mode 100644 index 3bec93a28..000000000 --- a/tool/kratos-gen-project/templates/http/internal/server/http/server.go.tmpl +++ /dev/null @@ -1,56 +0,0 @@ -package http - -import ( - "net/http" - - pb "{{.ModPrefix}}{{.Name}}/api" - "{{.ModPrefix}}{{.Name}}/internal/model" - "github.com/go-kratos/kratos/pkg/conf/paladin" - "github.com/go-kratos/kratos/pkg/log" - bm "github.com/go-kratos/kratos/pkg/net/http/blademaster" -) - -var svc pb.DemoServer - -// New new a bm server. -func New(s pb.DemoServer) (engine *bm.Engine, err error) { - var ( - cfg bm.ServerConfig - ct paladin.TOML - ) - if err = paladin.Get("http.toml").Unmarshal(&ct); err != nil { - return - } - if err = ct.Get("Server").UnmarshalTOML(&cfg); err != nil { - return - } - svc = s - engine = bm.DefaultServer(&cfg) - pb.RegisterDemoBMServer(engine, s) - initRouter(engine) - err = engine.Start() - return -} - -func initRouter(e *bm.Engine) { - e.Ping(ping) - g := e.Group("/{{.Name}}") - { - g.GET("/start", howToStart) - } -} - -func ping(ctx *bm.Context) { - if _, err := svc.Ping(ctx, nil); err != nil { - log.Error("ping error(%v)", err) - ctx.AbortWithStatus(http.StatusServiceUnavailable) - } -} - -// example for http request handler. -func howToStart(c *bm.Context) { - k := &model.Kratos{ - Hello: "Golang 大法好 !!!", - } - c.JSON(k, nil) -} \ No newline at end of file diff --git a/tool/kratos-gen-project/templates/http/internal/service/service.go.tmpl b/tool/kratos-gen-project/templates/http/internal/service/service.go.tmpl deleted file mode 100644 index f3750cc69..000000000 --- a/tool/kratos-gen-project/templates/http/internal/service/service.go.tmpl +++ /dev/null @@ -1,57 +0,0 @@ -package service - -import ( - "context" - "fmt" - - pb "{{.ModPrefix}}{{.Name}}/api" - "{{.ModPrefix}}{{.Name}}/internal/dao" - "github.com/go-kratos/kratos/pkg/conf/paladin" - - "github.com/golang/protobuf/ptypes/empty" - "github.com/google/wire" -) - -var Provider = wire.NewSet(New, wire.Bind(new(pb.DemoServer), new(*Service))) - -// Service service. -type Service struct { - ac *paladin.Map - dao dao.Dao -} - -// New new a service and return. -func New(d dao.Dao) (s *Service, cf func(), err error) { - s = &Service{ - ac: &paladin.TOML{}, - dao: d, - } - cf = s.Close - err = paladin.Watch("application.toml", s.ac) - return -} - -// SayHello grpc demo func. -func (s *Service) SayHello(ctx context.Context, req *pb.HelloReq) (reply *empty.Empty, err error) { - reply = new(empty.Empty) - fmt.Printf("hello %s", req.Name) - return -} - -// SayHelloURL bm demo func. -func (s *Service) SayHelloURL(ctx context.Context, req *pb.HelloReq) (reply *pb.HelloResp, err error) { - reply = &pb.HelloResp{ - Content: "hello " + req.Name, - } - fmt.Printf("hello url %s", req.Name) - return -} - -// Ping ping the resource. -func (s *Service) Ping(ctx context.Context, e *empty.Empty) (*empty.Empty, error) { - return &empty.Empty{}, s.dao.Ping(ctx) -} - -// Close close the resource. -func (s *Service) Close() { -} diff --git a/tool/kratos-gen-project/templates/http/test/0_db.sql b/tool/kratos-gen-project/templates/http/test/0_db.sql deleted file mode 100644 index e2cbccc19..000000000 --- a/tool/kratos-gen-project/templates/http/test/0_db.sql +++ /dev/null @@ -1,11 +0,0 @@ -create database kratos_demo; -use kratos_demo; - -CREATE TABLE `articles` ( - `id` int(11) UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '主键ID', - `title` varchar(64) NOT NULL COMMENT '名称', - `mtime` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '最后修改时间', - `ctime` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', - PRIMARY KEY (`id`), - KEY `ix_mtime` (`mtime`) -) COMMENT='文章表'; diff --git a/tool/kratos-gen-project/templates/http/test/1_data.sql b/tool/kratos-gen-project/templates/http/test/1_data.sql deleted file mode 100644 index c7cb57167..000000000 --- a/tool/kratos-gen-project/templates/http/test/1_data.sql +++ /dev/null @@ -1,3 +0,0 @@ -use kratos_demo; - -INSERT INTO articles(`id`, `title`) VALUES (1, 'title'); diff --git a/tool/kratos-gen-project/templates/http/test/application.toml b/tool/kratos-gen-project/templates/http/test/application.toml deleted file mode 100644 index a42ca6e63..000000000 --- a/tool/kratos-gen-project/templates/http/test/application.toml +++ /dev/null @@ -1,3 +0,0 @@ - -# This is a TOML document. Boom~ -demoExpire = "24h" diff --git a/tool/kratos-gen-project/templates/http/test/db.toml b/tool/kratos-gen-project/templates/http/test/db.toml deleted file mode 100644 index 9c7bd8618..000000000 --- a/tool/kratos-gen-project/templates/http/test/db.toml +++ /dev/null @@ -1,8 +0,0 @@ -[Client] - dsn = "root:root@tcp(127.0.0.1:13306)/kratos_demo?timeout=1s&readTimeout=1s&writeTimeout=1s&parseTime=true&loc=Local&charset=utf8mb4,utf8" - active = 20 - idle = 10 - idleTimeout ="4h" - queryTimeout = "200ms" - execTimeout = "300ms" - tranTimeout = "400ms" diff --git a/tool/kratos-gen-project/templates/http/test/docker-compose.yaml b/tool/kratos-gen-project/templates/http/test/docker-compose.yaml deleted file mode 100644 index b044d4247..000000000 --- a/tool/kratos-gen-project/templates/http/test/docker-compose.yaml +++ /dev/null @@ -1,40 +0,0 @@ - version: "3.7" - services: - db: - image: mysql:5.6 - ports: - - 13306:3306 - environment: - - MYSQL_ROOT_PASSWORD=root - - TZ=Asia/Shanghai - volumes: - - .:/docker-entrypoint-initdb.d - command: [ - '--character-set-server=utf8', - '--collation-server=utf8_unicode_ci' - ] - healthcheck: - test: ["CMD", "mysqladmin" ,"ping", "--protocol=tcp"] - timeout: 20s - interval: 1s - retries: 20 - - redis: - image: redis - ports: - - 16379:6379 - healthcheck: - test: ["CMD", "redis-cli","ping"] - interval: 20s - timeout: 1s - retries: 20 - - memcached: - image: memcached - ports: - - 21211:11211 - healthcheck: - test: ["CMD", "echo", "stats", "|", "nc", "127.0.0.1", "11211"] - interval: 20s - timeout: 1s - retries: 20 diff --git a/tool/kratos-gen-project/templates/http/test/http.toml b/tool/kratos-gen-project/templates/http/test/http.toml deleted file mode 100644 index 951a03197..000000000 --- a/tool/kratos-gen-project/templates/http/test/http.toml +++ /dev/null @@ -1,3 +0,0 @@ -[Server] - addr = "0.0.0.0:8000" - timeout = "1s" diff --git a/tool/kratos-gen-project/templates/http/test/memcache.toml b/tool/kratos-gen-project/templates/http/test/memcache.toml deleted file mode 100644 index ad2ce9e09..000000000 --- a/tool/kratos-gen-project/templates/http/test/memcache.toml +++ /dev/null @@ -1,10 +0,0 @@ -[Client] - name = "demo" - proto = "tcp" - addr = "127.0.0.1:21211" - active = 50 - idle = 10 - dialTimeout = "100ms" - readTimeout = "200ms" - writeTimeout = "300ms" - idleTimeout = "80s" diff --git a/tool/kratos-gen-project/templates/http/test/redis.toml b/tool/kratos-gen-project/templates/http/test/redis.toml deleted file mode 100644 index 371f243da..000000000 --- a/tool/kratos-gen-project/templates/http/test/redis.toml +++ /dev/null @@ -1,10 +0,0 @@ -[Client] - name = "demo" - proto = "tcp" - addr = "127.0.0.1:16379" - idle = 10 - active = 10 - dialTimeout = "1s" - readTimeout = "1s" - writeTimeout = "1s" - idleTimeout = "10s" diff --git a/tool/kratos-gen-project/testdata/test_in_gomod.sh b/tool/kratos-gen-project/testdata/test_in_gomod.sh deleted file mode 100755 index b2fb82a96..000000000 --- a/tool/kratos-gen-project/testdata/test_in_gomod.sh +++ /dev/null @@ -1,37 +0,0 @@ -#!/usr/bin/env bash -set -e - -dir=`pwd` - -cd $dir -rm -rf ./a -kratos new a -cd ./a/cmd && go build -if [ $? -ne 0 ]; then - echo "Failed: all" - exit 1 -else - rm -rf ../../a -fi - -cd $dir -rm -rf ./b -kratos new b --grpc -cd ./b/cmd && go build -if [ $? -ne 0 ];then - echo "Failed: --grpc" - exit 1 -else - rm -rf ../../b -fi - -cd $dir -rm -rf ./c -kratos new c --http -cd ./c/cmd && go build -if [ $? -ne 0 ]; then - echo "Failed: --http" - exit 1 -else - rm -rf ../../c -fi diff --git a/tool/kratos-gen-project/testdata/test_not_in_gomod.sh b/tool/kratos-gen-project/testdata/test_not_in_gomod.sh deleted file mode 100755 index 2d05a131a..000000000 --- a/tool/kratos-gen-project/testdata/test_not_in_gomod.sh +++ /dev/null @@ -1,41 +0,0 @@ -#!/usr/bin/env bash -set -e - -dir=/tmp/test-kratos -rm -rf $dir -mkdir $dir - -cd $dir -rm -rf ./a -kratos new a -cd ./a/cmd && go build -if [ $? -ne 0 ]; then - echo "Failed: all" - exit 1 -else - rm -rf ../../a -fi - -cd $dir -rm -rf ./b -kratos new b --grpc -cd ./b/cmd && go build -if [ $? -ne 0 ];then - echo "Failed: --grpc" - exit 1 -else - rm -rf ../../b -fi - -cd $dir -rm -rf ./c -kratos new c --http -cd ./c/cmd && go build -if [ $? -ne 0 ]; then - echo "Failed: --http" - exit 1 -else - rm -rf ../../c -fi - -rm -rf $dir diff --git a/tool/kratos-protoc/bm.go b/tool/kratos-protoc/bm.go deleted file mode 100644 index 284e713da..000000000 --- a/tool/kratos-protoc/bm.go +++ /dev/null @@ -1,23 +0,0 @@ -package main - -import ( - "os/exec" -) - -const ( - _getBMGen = "go get -u github.com/go-kratos/kratos/tool/protobuf/protoc-gen-bm" - _bmProtoc = "protoc --proto_path=%s --proto_path=%s --proto_path=%s --bm_out=:." -) - -func installBMGen() error { - if _, err := exec.LookPath("protoc-gen-bm"); err != nil { - if err := goget(_getBMGen); err != nil { - return err - } - } - return nil -} - -func genBM(files []string) error { - return generate(_bmProtoc, files) -} diff --git a/tool/kratos-protoc/ecode.go b/tool/kratos-protoc/ecode.go deleted file mode 100644 index eed3cf367..000000000 --- a/tool/kratos-protoc/ecode.go +++ /dev/null @@ -1,28 +0,0 @@ -package main - -import ( - "os/exec" -) - -const ( - _getEcodeGen = "go get -u github.com/go-kratos/kratos/tool/protobuf/protoc-gen-ecode" - _ecodeProtoc = "protoc --proto_path=%s --proto_path=%s --proto_path=%s --ecode_out=" + - "Mgoogle/protobuf/any.proto=github.com/gogo/protobuf/types," + - "Mgoogle/protobuf/duration.proto=github.com/gogo/protobuf/types," + - "Mgoogle/protobuf/struct.proto=github.com/gogo/protobuf/types," + - "Mgoogle/protobuf/timestamp.proto=github.com/gogo/protobuf/types," + - "Mgoogle/protobuf/wrappers.proto=github.com/gogo/protobuf/types:." -) - -func installEcodeGen() error { - if _, err := exec.LookPath("protoc-gen-ecode"); err != nil { - if err := goget(_getEcodeGen); err != nil { - return err - } - } - return nil -} - -func genEcode(files []string) error { - return generate(_ecodeProtoc, files) -} diff --git a/tool/kratos-protoc/grpc.go b/tool/kratos-protoc/grpc.go deleted file mode 100644 index f81cdcd12..000000000 --- a/tool/kratos-protoc/grpc.go +++ /dev/null @@ -1,28 +0,0 @@ -package main - -import ( - "os/exec" -) - -const ( - _getGRPCGen = "go get -u github.com/gogo/protobuf/protoc-gen-gofast" - _grpcProtoc = "protoc --proto_path=%s --proto_path=%s --proto_path=%s --gofast_out=plugins=grpc," + - "Mgoogle/protobuf/any.proto=github.com/gogo/protobuf/types," + - "Mgoogle/protobuf/duration.proto=github.com/gogo/protobuf/types," + - "Mgoogle/protobuf/struct.proto=github.com/gogo/protobuf/types," + - "Mgoogle/protobuf/timestamp.proto=github.com/gogo/protobuf/types," + - "Mgoogle/protobuf/wrappers.proto=github.com/gogo/protobuf/types:." -) - -func installGRPCGen() error { - if _, err := exec.LookPath("protoc-gen-gofast"); err != nil { - if err := goget(_getGRPCGen); err != nil { - return err - } - } - return nil -} - -func genGRPC(files []string) error { - return generate(_grpcProtoc, files) -} diff --git a/tool/kratos-protoc/main.go b/tool/kratos-protoc/main.go deleted file mode 100644 index 981d9857c..000000000 --- a/tool/kratos-protoc/main.go +++ /dev/null @@ -1,42 +0,0 @@ -package main - -import ( - "log" - "os" - - "github.com/urfave/cli/v2" -) - -func main() { - app := cli.NewApp() - app.Name = "protc" - app.Usage = "protobuf生成工具" - app.Flags = []cli.Flag{ - &cli.BoolFlag{ - Name: "bm", - Usage: "whether to use BM for generation", - Destination: &withBM, - }, - &cli.BoolFlag{ - Name: "grpc", - Usage: "whether to use gRPC for generation", - Destination: &withGRPC, - }, - &cli.BoolFlag{ - Name: "swagger", - Usage: "whether to use swagger for generation", - Destination: &withSwagger, - }, - &cli.BoolFlag{ - Name: "ecode", - Usage: "whether to use ecode for generation", - Destination: &withEcode, - }, - } - app.Action = func(c *cli.Context) error { - return protocAction(c) - } - if err := app.Run(os.Args); err != nil { - log.Fatal(err) - } -} diff --git a/tool/kratos-protoc/protoc.go b/tool/kratos-protoc/protoc.go deleted file mode 100644 index 91e3a460d..000000000 --- a/tool/kratos-protoc/protoc.go +++ /dev/null @@ -1,181 +0,0 @@ -package main - -import ( - "errors" - "fmt" - "go/build" - "io/ioutil" - "log" - "os" - "os/exec" - "path" - "path/filepath" - "runtime" - "strings" - - "github.com/urfave/cli/v2" -) - -var ( - withBM bool - withGRPC bool - withSwagger bool - withEcode bool -) - -func protocAction(ctx *cli.Context) (err error) { - if err = checkProtoc(); err != nil { - return err - } - files := ctx.Args().Slice() - if len(files) == 0 { - files, _ = filepath.Glob("*.proto") - } - if !withGRPC && !withBM && !withSwagger && !withEcode { - withBM = true - withGRPC = true - withSwagger = true - withEcode = true - } - if withBM { - if err = installBMGen(); err != nil { - return - } - if err = genBM(files); err != nil { - return - } - } - if withGRPC { - if err = installGRPCGen(); err != nil { - return err - } - if err = genGRPC(files); err != nil { - return - } - } - if withSwagger { - if err = installSwaggerGen(); err != nil { - return - } - if err = genSwagger(files); err != nil { - return - } - } - if withEcode { - if err = installEcodeGen(); err != nil { - return - } - if err = genEcode(files); err != nil { - return - } - } - log.Printf("generate %s success.\n", strings.Join(files, " ")) - return nil -} - -func checkProtoc() error { - if _, err := exec.LookPath("protoc"); err != nil { - switch runtime.GOOS { - case "darwin": - fmt.Println("brew install protobuf") - cmd := exec.Command("brew", "install", "protobuf") - cmd.Stdout = os.Stdout - cmd.Stderr = os.Stderr - if err = cmd.Run(); err != nil { - return err - } - case "linux": - fmt.Println("snap install --classic protobuf") - cmd := exec.Command("snap", "install", "--classic", "protobuf") - cmd.Stdout = os.Stdout - cmd.Stderr = os.Stderr - if err = cmd.Run(); err != nil { - return err - } - default: - return errors.New("您还没安装protobuf,请进行手动安装:https://github.com/protocolbuffers/protobuf/releases") - } - } - return nil -} - -func generate(protoc string, files []string) error { - pwd, _ := os.Getwd() - gosrc := path.Join(gopath(), "src") - ext, err := latestKratos() - if err != nil { - return err - } - line := fmt.Sprintf(protoc, gosrc, ext, pwd) - log.Println(line, strings.Join(files, " ")) - args := strings.Split(line, " ") - args = append(args, files...) - cmd := exec.Command(args[0], args[1:]...) - cmd.Dir = pwd - cmd.Env = os.Environ() - cmd.Stdout = os.Stdout - cmd.Stderr = os.Stderr - return cmd.Run() -} - -func goget(url string) error { - args := strings.Split(url, " ") - cmd := exec.Command(args[0], args[1:]...) - cmd.Env = os.Environ() - cmd.Stdout = os.Stdout - cmd.Stderr = os.Stderr - log.Println(url) - return cmd.Run() -} - -func latestKratos() (string, error) { - gopath := gopath() - ext := path.Join(gopath, "src/github.com/go-kratos/kratos/third_party") - if _, err := os.Stat(ext); !os.IsNotExist(err) { - return ext, nil - } - ext = path.Join(gopath, "src/kratos/third_party") - if _, err := os.Stat(ext); !os.IsNotExist(err) { - return ext, nil - } - baseMod := path.Join(gopath, "pkg/mod/github.com/go-kratos") - files, err := ioutil.ReadDir(baseMod) - if err != nil { - return "", err - } - for i := len(files) - 1; i >= 0; i-- { - if strings.HasPrefix(files[i].Name(), "kratos@") { - return path.Join(baseMod, files[i].Name(), "third_party"), nil - } - } - return "", errors.New("not found kratos package") -} - -func gopath() (gp string) { - gopaths := strings.Split(os.Getenv("GOPATH"), string(filepath.ListSeparator)) - - if len(gopaths) == 1 && gopaths[0] != "" { - return gopaths[0] - } - pwd, err := os.Getwd() - if err != nil { - return - } - abspwd, err := filepath.Abs(pwd) - if err != nil { - return - } - for _, gopath := range gopaths { - if gopath == "" { - continue - } - absgp, err := filepath.Abs(gopath) - if err != nil { - return - } - if strings.HasPrefix(abspwd, absgp) { - return absgp - } - } - return build.Default.GOPATH -} diff --git a/tool/kratos-protoc/swagger.go b/tool/kratos-protoc/swagger.go deleted file mode 100644 index b0f529a5a..000000000 --- a/tool/kratos-protoc/swagger.go +++ /dev/null @@ -1,28 +0,0 @@ -package main - -import ( - "os/exec" -) - -const ( - _getSwaggerGen = "go get -u github.com/go-kratos/kratos/tool/protobuf/protoc-gen-bswagger" - _swaggerProtoc = "protoc --proto_path=%s --proto_path=%s --proto_path=%s --bswagger_out=" + - "Mgoogle/protobuf/any.proto=github.com/gogo/protobuf/types," + - "Mgoogle/protobuf/duration.proto=github.com/gogo/protobuf/types," + - "Mgoogle/protobuf/struct.proto=github.com/gogo/protobuf/types," + - "Mgoogle/protobuf/timestamp.proto=github.com/gogo/protobuf/types," + - "Mgoogle/protobuf/wrappers.proto=github.com/gogo/protobuf/types:." -) - -func installSwaggerGen() error { - if _, err := exec.LookPath("protoc-gen-bswagger"); err != nil { - if err := goget(_getSwaggerGen); err != nil { - return err - } - } - return nil -} - -func genSwagger(files []string) error { - return generate(_swaggerProtoc, files) -} diff --git a/tool/kratos/README.MD b/tool/kratos/README.MD deleted file mode 100644 index b5e6da240..000000000 --- a/tool/kratos/README.MD +++ /dev/null @@ -1,14 +0,0 @@ -# kratos - -## 项目简介 -kratos 工具 - -## 安装 - -`go get -u github.com/go-kratos/kratos/tool/kratos` - -## 使用说明 - -### 参数 - -kratos -h diff --git a/tool/kratos/build.go b/tool/kratos/build.go deleted file mode 100644 index f0e8ec291..000000000 --- a/tool/kratos/build.go +++ /dev/null @@ -1,47 +0,0 @@ -package main - -import ( - "fmt" - "io/ioutil" - "os" - "os/exec" - "path" - "path/filepath" - - "github.com/urfave/cli/v2" -) - -func buildAction(c *cli.Context) error { - base, err := os.Getwd() - if err != nil { - panic(err) - } - args := append([]string{"build"}, c.Args().Slice()...) - cmd := exec.Command("go", args...) - cmd.Dir = buildDir(base, "cmd", 5) - cmd.Stdout = os.Stdout - cmd.Stderr = os.Stderr - fmt.Printf("directory: %s\n", cmd.Dir) - fmt.Printf("kratos: %s\n", Version) - if err := cmd.Run(); err != nil { - panic(err) - } - fmt.Println("build success.") - return nil -} - -func buildDir(base string, cmd string, n int) string { - dirs, err := ioutil.ReadDir(base) - if err != nil { - panic(err) - } - for _, d := range dirs { - if d.IsDir() && d.Name() == cmd { - return path.Join(base, cmd) - } - } - if n <= 1 { - return base - } - return buildDir(filepath.Dir(base), cmd, n-1) -} diff --git a/tool/kratos/env.go b/tool/kratos/env.go deleted file mode 100644 index 8447076fc..000000000 --- a/tool/kratos/env.go +++ /dev/null @@ -1,78 +0,0 @@ -package main - -import ( - "bytes" - "fmt" - "io/ioutil" - "os" - "path/filepath" - "sync" -) - -var envCache struct { - once sync.Once - m map[string]string -} - -// EnvFile returns the name of the Go environment configuration file. -func EnvFile() (string, error) { - if file := os.Getenv("GOENV"); file != "" { - if file == "off" { - return "", fmt.Errorf("GOENV=off") - } - return file, nil - } - dir, err := os.UserConfigDir() - if err != nil { - return "", err - } - if dir == "" { - return "", fmt.Errorf("missing user-config dir") - } - return filepath.Join(dir, "go/env"), nil -} - -func initEnvCache() { - envCache.m = make(map[string]string) - file, _ := EnvFile() - if file == "" { - return - } - data, err := ioutil.ReadFile(file) - if err != nil { - return - } - - for len(data) > 0 { - // Get next line. - line := data - i := bytes.IndexByte(data, '\n') - if i >= 0 { - line, data = line[:i], data[i+1:] - } else { - data = nil - } - - i = bytes.IndexByte(line, '=') - if i < 0 || line[0] < 'A' || 'Z' < line[0] { - // Line is missing = (or empty) or a comment or not a valid env name. Ignore. - // (This should not happen, since the file should be maintained almost - // exclusively by "go env -w", but better to silently ignore than to make - // the go command unusable just because somehow the env file has - // gotten corrupted.) - continue - } - key, val := line[:i], line[i+1:] - envCache.m[string(key)] = string(val) - } -} - -// Getenv gets the value from env or configuration. -func Getenv(key string) string { - val := os.Getenv(key) - if val != "" { - return val - } - envCache.once.Do(initEnvCache) - return envCache.m[key] -} diff --git a/tool/kratos/main.go b/tool/kratos/main.go deleted file mode 100644 index a6d87bcac..000000000 --- a/tool/kratos/main.go +++ /dev/null @@ -1,65 +0,0 @@ -package main - -import ( - "fmt" - "os" - - "github.com/urfave/cli/v2" -) - -func main() { - app := cli.NewApp() - app.Name = "kratos" - app.Usage = "kratos工具集" - app.Version = Version - app.Commands = []*cli.Command{ - { - Name: "new", - Aliases: []string{"n"}, - Usage: "创建新项目", - Action: runNew, - SkipFlagParsing: true, - }, - { - Name: "build", - Aliases: []string{"b"}, - Usage: "kratos build", - Action: buildAction, - }, - { - Name: "run", - Aliases: []string{"r"}, - Usage: "kratos run", - Action: runAction, - }, - { - Name: "tool", - Aliases: []string{"t"}, - Usage: "kratos tool", - Action: toolAction, - SkipFlagParsing: true, - }, - { - Name: "version", - Aliases: []string{"v"}, - Usage: "kratos version", - Action: func(c *cli.Context) error { - fmt.Println(getVersion()) - return nil - }, - }, - { - Name: "self-upgrade", - Usage: "kratos self-upgrade", - Action: upgradeAction, - }, - } - err := app.Run(os.Args) - if err != nil { - panic(err) - } -} - -func runNew(ctx *cli.Context) error { - return installAndRun("genproject", ctx.Args().Slice()) -} diff --git a/tool/kratos/run.go b/tool/kratos/run.go deleted file mode 100644 index e0ae3be99..000000000 --- a/tool/kratos/run.go +++ /dev/null @@ -1,28 +0,0 @@ -package main - -import ( - "os" - "os/exec" - "path" - "path/filepath" - - "github.com/urfave/cli/v2" -) - -func runAction(c *cli.Context) error { - base, err := os.Getwd() - if err != nil { - panic(err) - } - dir := buildDir(base, "cmd", 5) - conf := path.Join(filepath.Dir(dir), "configs") - args := append([]string{"run", "main.go", "-conf", conf}, c.Args().Slice()...) - cmd := exec.Command("go", args...) - cmd.Dir = dir - cmd.Stdout = os.Stdout - cmd.Stderr = os.Stderr - if err := cmd.Run(); err != nil { - panic(err) - } - return nil -} diff --git a/tool/kratos/tool.go b/tool/kratos/tool.go deleted file mode 100644 index 0c38224c2..000000000 --- a/tool/kratos/tool.go +++ /dev/null @@ -1,284 +0,0 @@ -package main - -import ( - "fmt" - "go/build" - "os" - "os/exec" - "path" - "path/filepath" - "runtime" - "sort" - "strings" - "time" - - "github.com/fatih/color" - "github.com/urfave/cli/v2" -) - -const ( - toolDoc = "https://go-kratos.github.io/kratos/#/kratos-tool" -) - -// Tool is kratos tool. -type Tool struct { - Name string `json:"name"` - Alias string `json:"alias"` - BuildTime time.Time `json:"build_time"` - Install string `json:"install"` - Requirements []string `json:"requirements"` - Dir string `json:"dir"` - Summary string `json:"summary"` - Platform []string `json:"platform"` - Author string `json:"author"` - URL string `json:"url"` - Hidden bool `json:"hidden"` - requires []*Tool -} - -func toolAction(c *cli.Context) (err error) { - if c.NArg() == 0 { - sort.Slice(toolIndexs, func(i, j int) bool { return toolIndexs[i].BuildTime.After(toolIndexs[j].BuildTime) }) - for _, t := range toolIndexs { - if t.Hidden { - continue - } - updateTime := t.BuildTime.Format("2006/01/02") - fmt.Printf("%s%s: %s Author(%s) [%s]\n", color.HiMagentaString(t.Name), getNotice(t), color.HiCyanString(t.Summary), t.Author, updateTime) - } - fmt.Println("\n安装工具: kratos tool install demo") - fmt.Println("执行工具: kratos tool demo") - fmt.Println("安装全部工具: kratos tool install all") - fmt.Println("全部升级: kratos tool upgrade all") - fmt.Println("\n详细文档:", toolDoc) - return - } - commond := c.Args().First() - switch commond { - case "upgrade": - upgradeAll() - return - case "install": - name := c.Args().Get(1) - if name == "all" { - installAll() - } else { - install(name) - } - return - case "check_install": - if e := checkInstall(c.Args().Get(1)); e != nil { - fmt.Fprintf(os.Stderr, fmt.Sprintf("%v\n", e)) - } - return - } - if e := installAndRun(commond, c.Args().Slice()[1:]); e != nil { - fmt.Fprintf(os.Stderr, fmt.Sprintf("%v\n", e)) - } - return -} - -func installAndRun(name string, args []string) (err error) { - for _, t := range toolList() { - if name == t.Name { - if !t.installed() || t.needUpdated() { - t.install() - } - pwd, _ := os.Getwd() - err = runTool(t.Name, pwd, t.toolPath(), args) - return - } - } - return fmt.Errorf("找不到%s", name) -} - -func checkInstall(name string) (err error) { - for _, t := range toolList() { - if name == t.Name { - if !t.installed() || t.needUpdated() { - t.install() - } - return - } - } - return fmt.Errorf("找不到%s", name) -} - -func upgradeAction(c *cli.Context) error { - install("kratos") - return nil -} - -func install(name string) { - if name == "" { - fmt.Fprintf(os.Stderr, color.HiRedString("请填写要安装的工具名称\n")) - return - } - for _, t := range toolList() { - if name == t.Name { - t.install() - return - } - } - fmt.Fprintf(os.Stderr, color.HiRedString("安装失败 找不到 %s\n", name)) - return -} - -func installAll() { - for _, t := range toolList() { - if t.Install != "" { - t.install() - } - } -} - -func upgradeAll() { - for _, t := range toolList() { - if t.needUpdated() { - t.install() - } - } -} - -func toolList() (tools []*Tool) { - return toolIndexs -} - -func getNotice(t *Tool) (notice string) { - if !t.supportOS() || t.Install == "" { - return - } - notice = color.HiGreenString("(未安装)") - if t.installed() { - notice = color.HiBlueString("(已安装)") - if t.needUpdated() { - notice = color.RedString("(有更新)") - } - } - return -} - -func (t Tool) needUpdated() bool { - for _, r := range t.requires { - if r.needUpdated() { - return true - } - } - if !t.supportOS() || t.Install == "" { - return false - } - if f, err := os.Stat(t.toolPath()); err == nil { - if t.BuildTime.After(f.ModTime()) { - return true - } - } - return false -} - -func (t Tool) toolPath() string { - name := t.Alias - if name == "" { - name = t.Name - } - gobin := Getenv("GOBIN") - if runtime.GOOS == "windows" { - name += ".exe" - } - if gobin != "" { - return filepath.Join(gobin, name) - } - return filepath.Join(gopath(), "bin", name) -} - -func (t Tool) installed() bool { - _, err := os.Stat(t.toolPath()) - return err == nil -} - -func (t Tool) supportOS() bool { - for _, p := range t.Platform { - if strings.ToLower(p) == runtime.GOOS { - return true - } - } - return false -} - -func (t Tool) install() { - if t.Install == "" { - fmt.Fprintf(os.Stderr, color.RedString("%s: 自动安装失败详情请查看文档:%s\n", t.Name, toolDoc)) - return - } - fmt.Println(t.Install) - cmds := strings.Split(t.Install, " ") - if len(cmds) > 0 { - if err := runTool(t.Name, path.Dir(t.toolPath()), cmds[0], cmds[1:]); err == nil { - color.Green("%s: 安装成功!", t.Name) - } - } -} - -func (t Tool) updated() bool { - if !t.supportOS() || t.Install == "" { - return false - } - if f, err := os.Stat(t.toolPath()); err == nil { - if t.BuildTime.After(f.ModTime()) { - return true - } - } - return false -} - -func gopath() (gp string) { - gopaths := strings.Split(Getenv("GOPATH"), string(filepath.ListSeparator)) - - if len(gopaths) == 1 && gopaths[0] != "" { - return gopaths[0] - } - pwd, err := os.Getwd() - if err != nil { - return - } - abspwd, err := filepath.Abs(pwd) - if err != nil { - return - } - for _, gopath := range gopaths { - if gopath == "" { - continue - } - absgp, err := filepath.Abs(gopath) - if err != nil { - return - } - if strings.HasPrefix(abspwd, absgp) { - return absgp - } - } - return build.Default.GOPATH -} - -func runTool(name, dir, cmd string, args []string) (err error) { - toolCmd := &exec.Cmd{ - Path: cmd, - Args: append([]string{cmd}, args...), - Dir: dir, - Stdin: os.Stdin, - Stdout: os.Stdout, - Stderr: os.Stderr, - Env: os.Environ(), - } - if filepath.Base(cmd) == cmd { - var lp string - if lp, err = exec.LookPath(cmd); err == nil { - toolCmd.Path = lp - } - } - if err = toolCmd.Run(); err != nil { - if e, ok := err.(*exec.ExitError); !ok || !e.Exited() { - fmt.Fprintf(os.Stderr, "运行 %s 出错: %v\n", name, err) - } - } - return -} diff --git a/tool/kratos/tool_index.go b/tool/kratos/tool_index.go deleted file mode 100644 index 46b1f9d7a..000000000 --- a/tool/kratos/tool_index.go +++ /dev/null @@ -1,88 +0,0 @@ -package main - -import "time" - -var toolIndexs = []*Tool{ - { - Name: "kratos", - Alias: "kratos", - BuildTime: time.Date(2020, 3, 31, 0, 0, 0, 0, time.Local), - Install: "go get -u github.com/go-kratos/kratos/tool/kratos@" + Version, - Summary: "Kratos工具集本体", - Platform: []string{"darwin", "linux", "windows"}, - Author: "kratos", - Hidden: true, - }, - { - Name: "protoc", - Alias: "kratos-protoc", - BuildTime: time.Date(2020, 3, 31, 0, 0, 0, 0, time.Local), - Install: "go get -u github.com/go-kratos/kratos/tool/kratos-protoc@" + Version, - Summary: "快速方便生成pb.go的protoc封装,windows、Linux请先安装protoc工具", - Platform: []string{"darwin", "linux", "windows"}, - Author: "kratos", - }, - { - Name: "genbts", - Alias: "kratos-gen-bts", - BuildTime: time.Date(2020, 3, 31, 0, 0, 0, 0, time.Local), - Install: "go get -u github.com/go-kratos/kratos/tool/kratos-gen-bts@" + Version, - Summary: "缓存回源逻辑代码生成器", - Platform: []string{"darwin", "linux", "windows"}, - Author: "kratos", - }, - { - Name: "genmc", - Alias: "kratos-gen-mc", - BuildTime: time.Date(2020, 3, 31, 0, 0, 0, 0, time.Local), - Install: "go get -u github.com/go-kratos/kratos/tool/kratos-gen-mc@" + Version, - Summary: "mc缓存代码生成", - Platform: []string{"darwin", "linux", "windows"}, - Author: "kratos", - }, - { - Name: "genproject", - Alias: "kratos-gen-project", - Install: "go get -u github.com/go-kratos/kratos/tool/kratos-gen-project@" + Version, - BuildTime: time.Date(2020, 3, 31, 0, 0, 0, 0, time.Local), - Platform: []string{"darwin", "linux", "windows"}, - Hidden: true, - Requirements: []string{"wire"}, - }, - { - Name: "testgen", - Alias: "testgen", - BuildTime: time.Date(2020, 3, 31, 0, 0, 0, 0, time.Local), - Install: "go get -u github.com/go-kratos/kratos/tool/testgen@" + Version, - Summary: "测试代码生成", - Platform: []string{"darwin", "linux", "windows"}, - Author: "kratos", - }, - { - Name: "testcli", - Alias: "testcli", - BuildTime: time.Date(2020, 3, 31, 0, 0, 0, 0, time.Local), - Install: "go get -u github.com/go-kratos/kratos/tool/testcli@" + Version, - Summary: "测试代码运行", - Platform: []string{"darwin", "linux", "windows"}, - Author: "kratos", - }, - // third party - { - Name: "wire", - Alias: "wire", - BuildTime: time.Date(2020, 3, 31, 0, 0, 0, 0, time.Local), - Install: "go get -u github.com/google/wire/cmd/wire", - Platform: []string{"darwin", "linux", "windows"}, - Hidden: true, - }, - { - Name: "swagger", - Alias: "swagger", - BuildTime: time.Date(2020, 3, 31, 0, 0, 0, 0, time.Local), - Install: "go get -u github.com/go-swagger/go-swagger/cmd/swagger", - Summary: "swagger api文档", - Platform: []string{"darwin", "linux", "windows"}, - Author: "goswagger.io", - }, -} diff --git a/tool/kratos/version.go b/tool/kratos/version.go deleted file mode 100644 index b6120fb02..000000000 --- a/tool/kratos/version.go +++ /dev/null @@ -1,44 +0,0 @@ -package main - -import ( - "bytes" - "runtime" - "text/template" -) - -var ( - // Version is version - Version = "v0.6.0" - // BuildTime is BuildTime - BuildTime = "2020/12/4" -) - -// VersionOptions include version -type VersionOptions struct { - GitCommit string - Version string - BuildTime string - GoVersion string - Os string - Arch string -} - -var versionTemplate = ` Version: {{.Version}} - Go version: {{.GoVersion}} - Built: {{.BuildTime}} - OS/Arch: {{.Os}}/{{.Arch}} - ` - -func getVersion() string { - var doc bytes.Buffer - vo := VersionOptions{ - Version: Version, - BuildTime: BuildTime, - GoVersion: runtime.Version(), - Os: runtime.GOOS, - Arch: runtime.GOARCH, - } - tmpl, _ := template.New("version").Parse(versionTemplate) - tmpl.Execute(&doc, vo) - return doc.String() -} diff --git a/tool/pkg/common.go b/tool/pkg/common.go deleted file mode 100644 index 0941203f1..000000000 --- a/tool/pkg/common.go +++ /dev/null @@ -1,151 +0,0 @@ -package pkg - -import ( - "fmt" - "go/ast" - "go/format" - "go/parser" - "go/token" - "io/ioutil" - "log" - "os" - "regexp" - "strings" -) - -// Source source -type Source struct { - Fset *token.FileSet - Src string - F *ast.File -} - -// NewSource new source -func NewSource(src string) *Source { - s := &Source{ - Fset: token.NewFileSet(), - Src: src, - } - f, err := parser.ParseFile(s.Fset, "", src, 0) - if err != nil { - log.Fatal("无法解析源文件") - } - s.F = f - return s -} - -// ExprString expr string -func (s *Source) ExprString(typ ast.Expr) string { - fset := s.Fset - s1 := fset.Position(typ.Pos()).Offset - s2 := fset.Position(typ.End()).Offset - return s.Src[s1:s2] -} - -// pkgPath package path -func (s *Source) pkgPath(name string) (res string) { - for _, im := range s.F.Imports { - if im.Name != nil && im.Name.Name == name { - return im.Path.Value - } - } - for _, im := range s.F.Imports { - if strings.HasSuffix(im.Path.Value, name+"\"") { - return im.Path.Value - } - } - return -} - -// GetDef get define code -func (s *Source) GetDef(name string) string { - c := s.F.Scope.Lookup(name).Decl.(*ast.TypeSpec).Type.(*ast.InterfaceType) - s1 := s.Fset.Position(c.Pos()).Offset - s2 := s.Fset.Position(c.End()).Offset - line := s.Fset.Position(c.Pos()).Line - lines := []string{strings.Split(s.Src, "\n")[line-1]} - for _, l := range strings.Split(s.Src[s1:s2], "\n")[1:] { - lines = append(lines, "\t"+l) - } - return strings.Join(lines, "\n") -} - -// RegexpReplace replace regexp -func RegexpReplace(reg, src, temp string) string { - result := []byte{} - pattern := regexp.MustCompile(reg) - for _, submatches := range pattern.FindAllStringSubmatchIndex(src, -1) { - result = pattern.ExpandString(result, temp, src, submatches) - } - return string(result) -} - -// formatPackage format package -func formatPackage(name, path string) (res string) { - if path != "" { - if strings.HasSuffix(path, name+"\"") { - res = path - return - } - res = fmt.Sprintf("%s %s", name, path) - } - return -} - -// SourceText get source file text -func SourceText() string { - file := os.Getenv("GOFILE") - data, err := ioutil.ReadFile(file) - if err != nil { - log.Fatal("请使用go generate执行", file) - } - return string(data) -} - -// FormatCode format code -func FormatCode(source string) string { - src, err := format.Source([]byte(source)) - if err != nil { - // Should never happen, but can arise when developing this code. - // The user can compile the output to see the error. - log.Printf("warning: 输出文件不合法: %s", err) - log.Printf("warning: 详细错误请编译查看") - return source - } - return string(src) -} - -// Packages get import packages -func (s *Source) Packages(f *ast.Field) (res []string) { - fs := f.Type.(*ast.FuncType).Params.List - if f.Type.(*ast.FuncType).Results != nil { - fs = append(fs, f.Type.(*ast.FuncType).Results.List...) - } - var types []string - resMap := make(map[string]bool) - for _, field := range fs { - if p, ok := field.Type.(*ast.MapType); ok { - types = append(types, s.ExprString(p.Key)) - types = append(types, s.ExprString(p.Value)) - } else if p, ok := field.Type.(*ast.ArrayType); ok { - types = append(types, s.ExprString(p.Elt)) - } else { - types = append(types, s.ExprString(field.Type)) - } - } - - for _, t := range types { - name := RegexpReplace(`(?P\w+)\.\w+`, t, "$pkg") - if name == "" { - continue - } - pkg := formatPackage(name, s.pkgPath(name)) - if !resMap[pkg] { - resMap[pkg] = true - } - } - for pkg := range resMap { - res = append(res, pkg) - } - return -} diff --git a/tool/protobuf/pkg/extensions/gogoproto/gogo.pb.go b/tool/protobuf/pkg/extensions/gogoproto/gogo.pb.go deleted file mode 100644 index 7819be39d..000000000 --- a/tool/protobuf/pkg/extensions/gogoproto/gogo.pb.go +++ /dev/null @@ -1,818 +0,0 @@ -// Code generated by protoc-gen-go. DO NOT EDIT. -// source: gogo.proto - -package gogoproto // import "github.com/go-kratos/kratos/tool/protobuf/pkg/extensions/gogoproto" - -import proto "github.com/golang/protobuf/proto" -import fmt "fmt" -import math "math" -import descriptor "github.com/golang/protobuf/protoc-gen-go/descriptor" - -// Reference imports to suppress errors if they are not otherwise used. -var _ = proto.Marshal -var _ = fmt.Errorf -var _ = math.Inf - -// This is a compile-time assertion to ensure that this generated file -// is compatible with the proto package it is being compiled against. -// A compilation error at this line likely means your copy of the -// proto package needs to be updated. -const _ = proto.ProtoPackageIsVersion2 // please upgrade the proto package - -var E_GoprotoEnumPrefix = &proto.ExtensionDesc{ - ExtendedType: (*descriptor.EnumOptions)(nil), - ExtensionType: (*bool)(nil), - Field: 62001, - Name: "gogoproto.goproto_enum_prefix", - Tag: "varint,62001,opt,name=goproto_enum_prefix,json=goprotoEnumPrefix", - Filename: "gogo.proto", -} - -var E_GoprotoEnumStringer = &proto.ExtensionDesc{ - ExtendedType: (*descriptor.EnumOptions)(nil), - ExtensionType: (*bool)(nil), - Field: 62021, - Name: "gogoproto.goproto_enum_stringer", - Tag: "varint,62021,opt,name=goproto_enum_stringer,json=goprotoEnumStringer", - Filename: "gogo.proto", -} - -var E_EnumStringer = &proto.ExtensionDesc{ - ExtendedType: (*descriptor.EnumOptions)(nil), - ExtensionType: (*bool)(nil), - Field: 62022, - Name: "gogoproto.enum_stringer", - Tag: "varint,62022,opt,name=enum_stringer,json=enumStringer", - Filename: "gogo.proto", -} - -var E_EnumCustomname = &proto.ExtensionDesc{ - ExtendedType: (*descriptor.EnumOptions)(nil), - ExtensionType: (*string)(nil), - Field: 62023, - Name: "gogoproto.enum_customname", - Tag: "bytes,62023,opt,name=enum_customname,json=enumCustomname", - Filename: "gogo.proto", -} - -var E_Enumdecl = &proto.ExtensionDesc{ - ExtendedType: (*descriptor.EnumOptions)(nil), - ExtensionType: (*bool)(nil), - Field: 62024, - Name: "gogoproto.enumdecl", - Tag: "varint,62024,opt,name=enumdecl", - Filename: "gogo.proto", -} - -var E_EnumvalueCustomname = &proto.ExtensionDesc{ - ExtendedType: (*descriptor.EnumValueOptions)(nil), - ExtensionType: (*string)(nil), - Field: 66001, - Name: "gogoproto.enumvalue_customname", - Tag: "bytes,66001,opt,name=enumvalue_customname,json=enumvalueCustomname", - Filename: "gogo.proto", -} - -var E_GoprotoGettersAll = &proto.ExtensionDesc{ - ExtendedType: (*descriptor.FileOptions)(nil), - ExtensionType: (*bool)(nil), - Field: 63001, - Name: "gogoproto.goproto_getters_all", - Tag: "varint,63001,opt,name=goproto_getters_all,json=goprotoGettersAll", - Filename: "gogo.proto", -} - -var E_GoprotoEnumPrefixAll = &proto.ExtensionDesc{ - ExtendedType: (*descriptor.FileOptions)(nil), - ExtensionType: (*bool)(nil), - Field: 63002, - Name: "gogoproto.goproto_enum_prefix_all", - Tag: "varint,63002,opt,name=goproto_enum_prefix_all,json=goprotoEnumPrefixAll", - Filename: "gogo.proto", -} - -var E_GoprotoStringerAll = &proto.ExtensionDesc{ - ExtendedType: (*descriptor.FileOptions)(nil), - ExtensionType: (*bool)(nil), - Field: 63003, - Name: "gogoproto.goproto_stringer_all", - Tag: "varint,63003,opt,name=goproto_stringer_all,json=goprotoStringerAll", - Filename: "gogo.proto", -} - -var E_VerboseEqualAll = &proto.ExtensionDesc{ - ExtendedType: (*descriptor.FileOptions)(nil), - ExtensionType: (*bool)(nil), - Field: 63004, - Name: "gogoproto.verbose_equal_all", - Tag: "varint,63004,opt,name=verbose_equal_all,json=verboseEqualAll", - Filename: "gogo.proto", -} - -var E_FaceAll = &proto.ExtensionDesc{ - ExtendedType: (*descriptor.FileOptions)(nil), - ExtensionType: (*bool)(nil), - Field: 63005, - Name: "gogoproto.face_all", - Tag: "varint,63005,opt,name=face_all,json=faceAll", - Filename: "gogo.proto", -} - -var E_GostringAll = &proto.ExtensionDesc{ - ExtendedType: (*descriptor.FileOptions)(nil), - ExtensionType: (*bool)(nil), - Field: 63006, - Name: "gogoproto.gostring_all", - Tag: "varint,63006,opt,name=gostring_all,json=gostringAll", - Filename: "gogo.proto", -} - -var E_PopulateAll = &proto.ExtensionDesc{ - ExtendedType: (*descriptor.FileOptions)(nil), - ExtensionType: (*bool)(nil), - Field: 63007, - Name: "gogoproto.populate_all", - Tag: "varint,63007,opt,name=populate_all,json=populateAll", - Filename: "gogo.proto", -} - -var E_StringerAll = &proto.ExtensionDesc{ - ExtendedType: (*descriptor.FileOptions)(nil), - ExtensionType: (*bool)(nil), - Field: 63008, - Name: "gogoproto.stringer_all", - Tag: "varint,63008,opt,name=stringer_all,json=stringerAll", - Filename: "gogo.proto", -} - -var E_OnlyoneAll = &proto.ExtensionDesc{ - ExtendedType: (*descriptor.FileOptions)(nil), - ExtensionType: (*bool)(nil), - Field: 63009, - Name: "gogoproto.onlyone_all", - Tag: "varint,63009,opt,name=onlyone_all,json=onlyoneAll", - Filename: "gogo.proto", -} - -var E_EqualAll = &proto.ExtensionDesc{ - ExtendedType: (*descriptor.FileOptions)(nil), - ExtensionType: (*bool)(nil), - Field: 63013, - Name: "gogoproto.equal_all", - Tag: "varint,63013,opt,name=equal_all,json=equalAll", - Filename: "gogo.proto", -} - -var E_DescriptionAll = &proto.ExtensionDesc{ - ExtendedType: (*descriptor.FileOptions)(nil), - ExtensionType: (*bool)(nil), - Field: 63014, - Name: "gogoproto.description_all", - Tag: "varint,63014,opt,name=description_all,json=descriptionAll", - Filename: "gogo.proto", -} - -var E_TestgenAll = &proto.ExtensionDesc{ - ExtendedType: (*descriptor.FileOptions)(nil), - ExtensionType: (*bool)(nil), - Field: 63015, - Name: "gogoproto.testgen_all", - Tag: "varint,63015,opt,name=testgen_all,json=testgenAll", - Filename: "gogo.proto", -} - -var E_BenchgenAll = &proto.ExtensionDesc{ - ExtendedType: (*descriptor.FileOptions)(nil), - ExtensionType: (*bool)(nil), - Field: 63016, - Name: "gogoproto.benchgen_all", - Tag: "varint,63016,opt,name=benchgen_all,json=benchgenAll", - Filename: "gogo.proto", -} - -var E_MarshalerAll = &proto.ExtensionDesc{ - ExtendedType: (*descriptor.FileOptions)(nil), - ExtensionType: (*bool)(nil), - Field: 63017, - Name: "gogoproto.marshaler_all", - Tag: "varint,63017,opt,name=marshaler_all,json=marshalerAll", - Filename: "gogo.proto", -} - -var E_UnmarshalerAll = &proto.ExtensionDesc{ - ExtendedType: (*descriptor.FileOptions)(nil), - ExtensionType: (*bool)(nil), - Field: 63018, - Name: "gogoproto.unmarshaler_all", - Tag: "varint,63018,opt,name=unmarshaler_all,json=unmarshalerAll", - Filename: "gogo.proto", -} - -var E_StableMarshalerAll = &proto.ExtensionDesc{ - ExtendedType: (*descriptor.FileOptions)(nil), - ExtensionType: (*bool)(nil), - Field: 63019, - Name: "gogoproto.stable_marshaler_all", - Tag: "varint,63019,opt,name=stable_marshaler_all,json=stableMarshalerAll", - Filename: "gogo.proto", -} - -var E_SizerAll = &proto.ExtensionDesc{ - ExtendedType: (*descriptor.FileOptions)(nil), - ExtensionType: (*bool)(nil), - Field: 63020, - Name: "gogoproto.sizer_all", - Tag: "varint,63020,opt,name=sizer_all,json=sizerAll", - Filename: "gogo.proto", -} - -var E_GoprotoEnumStringerAll = &proto.ExtensionDesc{ - ExtendedType: (*descriptor.FileOptions)(nil), - ExtensionType: (*bool)(nil), - Field: 63021, - Name: "gogoproto.goproto_enum_stringer_all", - Tag: "varint,63021,opt,name=goproto_enum_stringer_all,json=goprotoEnumStringerAll", - Filename: "gogo.proto", -} - -var E_EnumStringerAll = &proto.ExtensionDesc{ - ExtendedType: (*descriptor.FileOptions)(nil), - ExtensionType: (*bool)(nil), - Field: 63022, - Name: "gogoproto.enum_stringer_all", - Tag: "varint,63022,opt,name=enum_stringer_all,json=enumStringerAll", - Filename: "gogo.proto", -} - -var E_UnsafeMarshalerAll = &proto.ExtensionDesc{ - ExtendedType: (*descriptor.FileOptions)(nil), - ExtensionType: (*bool)(nil), - Field: 63023, - Name: "gogoproto.unsafe_marshaler_all", - Tag: "varint,63023,opt,name=unsafe_marshaler_all,json=unsafeMarshalerAll", - Filename: "gogo.proto", -} - -var E_UnsafeUnmarshalerAll = &proto.ExtensionDesc{ - ExtendedType: (*descriptor.FileOptions)(nil), - ExtensionType: (*bool)(nil), - Field: 63024, - Name: "gogoproto.unsafe_unmarshaler_all", - Tag: "varint,63024,opt,name=unsafe_unmarshaler_all,json=unsafeUnmarshalerAll", - Filename: "gogo.proto", -} - -var E_GoprotoExtensionsMapAll = &proto.ExtensionDesc{ - ExtendedType: (*descriptor.FileOptions)(nil), - ExtensionType: (*bool)(nil), - Field: 63025, - Name: "gogoproto.goproto_extensions_map_all", - Tag: "varint,63025,opt,name=goproto_extensions_map_all,json=goprotoExtensionsMapAll", - Filename: "gogo.proto", -} - -var E_GoprotoUnrecognizedAll = &proto.ExtensionDesc{ - ExtendedType: (*descriptor.FileOptions)(nil), - ExtensionType: (*bool)(nil), - Field: 63026, - Name: "gogoproto.goproto_unrecognized_all", - Tag: "varint,63026,opt,name=goproto_unrecognized_all,json=goprotoUnrecognizedAll", - Filename: "gogo.proto", -} - -var E_GogoprotoImport = &proto.ExtensionDesc{ - ExtendedType: (*descriptor.FileOptions)(nil), - ExtensionType: (*bool)(nil), - Field: 63027, - Name: "gogoproto.gogoproto_import", - Tag: "varint,63027,opt,name=gogoproto_import,json=gogoprotoImport", - Filename: "gogo.proto", -} - -var E_ProtosizerAll = &proto.ExtensionDesc{ - ExtendedType: (*descriptor.FileOptions)(nil), - ExtensionType: (*bool)(nil), - Field: 63028, - Name: "gogoproto.protosizer_all", - Tag: "varint,63028,opt,name=protosizer_all,json=protosizerAll", - Filename: "gogo.proto", -} - -var E_CompareAll = &proto.ExtensionDesc{ - ExtendedType: (*descriptor.FileOptions)(nil), - ExtensionType: (*bool)(nil), - Field: 63029, - Name: "gogoproto.compare_all", - Tag: "varint,63029,opt,name=compare_all,json=compareAll", - Filename: "gogo.proto", -} - -var E_TypedeclAll = &proto.ExtensionDesc{ - ExtendedType: (*descriptor.FileOptions)(nil), - ExtensionType: (*bool)(nil), - Field: 63030, - Name: "gogoproto.typedecl_all", - Tag: "varint,63030,opt,name=typedecl_all,json=typedeclAll", - Filename: "gogo.proto", -} - -var E_EnumdeclAll = &proto.ExtensionDesc{ - ExtendedType: (*descriptor.FileOptions)(nil), - ExtensionType: (*bool)(nil), - Field: 63031, - Name: "gogoproto.enumdecl_all", - Tag: "varint,63031,opt,name=enumdecl_all,json=enumdeclAll", - Filename: "gogo.proto", -} - -var E_GoprotoRegistration = &proto.ExtensionDesc{ - ExtendedType: (*descriptor.FileOptions)(nil), - ExtensionType: (*bool)(nil), - Field: 63032, - Name: "gogoproto.goproto_registration", - Tag: "varint,63032,opt,name=goproto_registration,json=goprotoRegistration", - Filename: "gogo.proto", -} - -var E_MessagenameAll = &proto.ExtensionDesc{ - ExtendedType: (*descriptor.FileOptions)(nil), - ExtensionType: (*bool)(nil), - Field: 63033, - Name: "gogoproto.messagename_all", - Tag: "varint,63033,opt,name=messagename_all,json=messagenameAll", - Filename: "gogo.proto", -} - -var E_GoprotoGetters = &proto.ExtensionDesc{ - ExtendedType: (*descriptor.MessageOptions)(nil), - ExtensionType: (*bool)(nil), - Field: 64001, - Name: "gogoproto.goproto_getters", - Tag: "varint,64001,opt,name=goproto_getters,json=goprotoGetters", - Filename: "gogo.proto", -} - -var E_GoprotoStringer = &proto.ExtensionDesc{ - ExtendedType: (*descriptor.MessageOptions)(nil), - ExtensionType: (*bool)(nil), - Field: 64003, - Name: "gogoproto.goproto_stringer", - Tag: "varint,64003,opt,name=goproto_stringer,json=goprotoStringer", - Filename: "gogo.proto", -} - -var E_VerboseEqual = &proto.ExtensionDesc{ - ExtendedType: (*descriptor.MessageOptions)(nil), - ExtensionType: (*bool)(nil), - Field: 64004, - Name: "gogoproto.verbose_equal", - Tag: "varint,64004,opt,name=verbose_equal,json=verboseEqual", - Filename: "gogo.proto", -} - -var E_Face = &proto.ExtensionDesc{ - ExtendedType: (*descriptor.MessageOptions)(nil), - ExtensionType: (*bool)(nil), - Field: 64005, - Name: "gogoproto.face", - Tag: "varint,64005,opt,name=face", - Filename: "gogo.proto", -} - -var E_Gostring = &proto.ExtensionDesc{ - ExtendedType: (*descriptor.MessageOptions)(nil), - ExtensionType: (*bool)(nil), - Field: 64006, - Name: "gogoproto.gostring", - Tag: "varint,64006,opt,name=gostring", - Filename: "gogo.proto", -} - -var E_Populate = &proto.ExtensionDesc{ - ExtendedType: (*descriptor.MessageOptions)(nil), - ExtensionType: (*bool)(nil), - Field: 64007, - Name: "gogoproto.populate", - Tag: "varint,64007,opt,name=populate", - Filename: "gogo.proto", -} - -var E_Stringer = &proto.ExtensionDesc{ - ExtendedType: (*descriptor.MessageOptions)(nil), - ExtensionType: (*bool)(nil), - Field: 67008, - Name: "gogoproto.stringer", - Tag: "varint,67008,opt,name=stringer", - Filename: "gogo.proto", -} - -var E_Onlyone = &proto.ExtensionDesc{ - ExtendedType: (*descriptor.MessageOptions)(nil), - ExtensionType: (*bool)(nil), - Field: 64009, - Name: "gogoproto.onlyone", - Tag: "varint,64009,opt,name=onlyone", - Filename: "gogo.proto", -} - -var E_Equal = &proto.ExtensionDesc{ - ExtendedType: (*descriptor.MessageOptions)(nil), - ExtensionType: (*bool)(nil), - Field: 64013, - Name: "gogoproto.equal", - Tag: "varint,64013,opt,name=equal", - Filename: "gogo.proto", -} - -var E_Description = &proto.ExtensionDesc{ - ExtendedType: (*descriptor.MessageOptions)(nil), - ExtensionType: (*bool)(nil), - Field: 64014, - Name: "gogoproto.description", - Tag: "varint,64014,opt,name=description", - Filename: "gogo.proto", -} - -var E_Testgen = &proto.ExtensionDesc{ - ExtendedType: (*descriptor.MessageOptions)(nil), - ExtensionType: (*bool)(nil), - Field: 64015, - Name: "gogoproto.testgen", - Tag: "varint,64015,opt,name=testgen", - Filename: "gogo.proto", -} - -var E_Benchgen = &proto.ExtensionDesc{ - ExtendedType: (*descriptor.MessageOptions)(nil), - ExtensionType: (*bool)(nil), - Field: 64016, - Name: "gogoproto.benchgen", - Tag: "varint,64016,opt,name=benchgen", - Filename: "gogo.proto", -} - -var E_Marshaler = &proto.ExtensionDesc{ - ExtendedType: (*descriptor.MessageOptions)(nil), - ExtensionType: (*bool)(nil), - Field: 64017, - Name: "gogoproto.marshaler", - Tag: "varint,64017,opt,name=marshaler", - Filename: "gogo.proto", -} - -var E_Unmarshaler = &proto.ExtensionDesc{ - ExtendedType: (*descriptor.MessageOptions)(nil), - ExtensionType: (*bool)(nil), - Field: 64018, - Name: "gogoproto.unmarshaler", - Tag: "varint,64018,opt,name=unmarshaler", - Filename: "gogo.proto", -} - -var E_StableMarshaler = &proto.ExtensionDesc{ - ExtendedType: (*descriptor.MessageOptions)(nil), - ExtensionType: (*bool)(nil), - Field: 64019, - Name: "gogoproto.stable_marshaler", - Tag: "varint,64019,opt,name=stable_marshaler,json=stableMarshaler", - Filename: "gogo.proto", -} - -var E_Sizer = &proto.ExtensionDesc{ - ExtendedType: (*descriptor.MessageOptions)(nil), - ExtensionType: (*bool)(nil), - Field: 64020, - Name: "gogoproto.sizer", - Tag: "varint,64020,opt,name=sizer", - Filename: "gogo.proto", -} - -var E_UnsafeMarshaler = &proto.ExtensionDesc{ - ExtendedType: (*descriptor.MessageOptions)(nil), - ExtensionType: (*bool)(nil), - Field: 64023, - Name: "gogoproto.unsafe_marshaler", - Tag: "varint,64023,opt,name=unsafe_marshaler,json=unsafeMarshaler", - Filename: "gogo.proto", -} - -var E_UnsafeUnmarshaler = &proto.ExtensionDesc{ - ExtendedType: (*descriptor.MessageOptions)(nil), - ExtensionType: (*bool)(nil), - Field: 64024, - Name: "gogoproto.unsafe_unmarshaler", - Tag: "varint,64024,opt,name=unsafe_unmarshaler,json=unsafeUnmarshaler", - Filename: "gogo.proto", -} - -var E_GoprotoExtensionsMap = &proto.ExtensionDesc{ - ExtendedType: (*descriptor.MessageOptions)(nil), - ExtensionType: (*bool)(nil), - Field: 64025, - Name: "gogoproto.goproto_extensions_map", - Tag: "varint,64025,opt,name=goproto_extensions_map,json=goprotoExtensionsMap", - Filename: "gogo.proto", -} - -var E_GoprotoUnrecognized = &proto.ExtensionDesc{ - ExtendedType: (*descriptor.MessageOptions)(nil), - ExtensionType: (*bool)(nil), - Field: 64026, - Name: "gogoproto.goproto_unrecognized", - Tag: "varint,64026,opt,name=goproto_unrecognized,json=goprotoUnrecognized", - Filename: "gogo.proto", -} - -var E_Protosizer = &proto.ExtensionDesc{ - ExtendedType: (*descriptor.MessageOptions)(nil), - ExtensionType: (*bool)(nil), - Field: 64028, - Name: "gogoproto.protosizer", - Tag: "varint,64028,opt,name=protosizer", - Filename: "gogo.proto", -} - -var E_Compare = &proto.ExtensionDesc{ - ExtendedType: (*descriptor.MessageOptions)(nil), - ExtensionType: (*bool)(nil), - Field: 64029, - Name: "gogoproto.compare", - Tag: "varint,64029,opt,name=compare", - Filename: "gogo.proto", -} - -var E_Typedecl = &proto.ExtensionDesc{ - ExtendedType: (*descriptor.MessageOptions)(nil), - ExtensionType: (*bool)(nil), - Field: 64030, - Name: "gogoproto.typedecl", - Tag: "varint,64030,opt,name=typedecl", - Filename: "gogo.proto", -} - -var E_Messagename = &proto.ExtensionDesc{ - ExtendedType: (*descriptor.MessageOptions)(nil), - ExtensionType: (*bool)(nil), - Field: 64033, - Name: "gogoproto.messagename", - Tag: "varint,64033,opt,name=messagename", - Filename: "gogo.proto", -} - -var E_Nullable = &proto.ExtensionDesc{ - ExtendedType: (*descriptor.FieldOptions)(nil), - ExtensionType: (*bool)(nil), - Field: 65001, - Name: "gogoproto.nullable", - Tag: "varint,65001,opt,name=nullable", - Filename: "gogo.proto", -} - -var E_Embed = &proto.ExtensionDesc{ - ExtendedType: (*descriptor.FieldOptions)(nil), - ExtensionType: (*bool)(nil), - Field: 65002, - Name: "gogoproto.embed", - Tag: "varint,65002,opt,name=embed", - Filename: "gogo.proto", -} - -var E_Customtype = &proto.ExtensionDesc{ - ExtendedType: (*descriptor.FieldOptions)(nil), - ExtensionType: (*string)(nil), - Field: 65003, - Name: "gogoproto.customtype", - Tag: "bytes,65003,opt,name=customtype", - Filename: "gogo.proto", -} - -var E_Customname = &proto.ExtensionDesc{ - ExtendedType: (*descriptor.FieldOptions)(nil), - ExtensionType: (*string)(nil), - Field: 65004, - Name: "gogoproto.customname", - Tag: "bytes,65004,opt,name=customname", - Filename: "gogo.proto", -} - -var E_Jsontag = &proto.ExtensionDesc{ - ExtendedType: (*descriptor.FieldOptions)(nil), - ExtensionType: (*string)(nil), - Field: 65005, - Name: "gogoproto.jsontag", - Tag: "bytes,65005,opt,name=jsontag", - Filename: "gogo.proto", -} - -var E_Moretags = &proto.ExtensionDesc{ - ExtendedType: (*descriptor.FieldOptions)(nil), - ExtensionType: (*string)(nil), - Field: 65006, - Name: "gogoproto.moretags", - Tag: "bytes,65006,opt,name=moretags", - Filename: "gogo.proto", -} - -var E_Casttype = &proto.ExtensionDesc{ - ExtendedType: (*descriptor.FieldOptions)(nil), - ExtensionType: (*string)(nil), - Field: 65007, - Name: "gogoproto.casttype", - Tag: "bytes,65007,opt,name=casttype", - Filename: "gogo.proto", -} - -var E_Castkey = &proto.ExtensionDesc{ - ExtendedType: (*descriptor.FieldOptions)(nil), - ExtensionType: (*string)(nil), - Field: 65008, - Name: "gogoproto.castkey", - Tag: "bytes,65008,opt,name=castkey", - Filename: "gogo.proto", -} - -var E_Castvalue = &proto.ExtensionDesc{ - ExtendedType: (*descriptor.FieldOptions)(nil), - ExtensionType: (*string)(nil), - Field: 65009, - Name: "gogoproto.castvalue", - Tag: "bytes,65009,opt,name=castvalue", - Filename: "gogo.proto", -} - -var E_Stdtime = &proto.ExtensionDesc{ - ExtendedType: (*descriptor.FieldOptions)(nil), - ExtensionType: (*bool)(nil), - Field: 65010, - Name: "gogoproto.stdtime", - Tag: "varint,65010,opt,name=stdtime", - Filename: "gogo.proto", -} - -var E_Stdduration = &proto.ExtensionDesc{ - ExtendedType: (*descriptor.FieldOptions)(nil), - ExtensionType: (*bool)(nil), - Field: 65011, - Name: "gogoproto.stdduration", - Tag: "varint,65011,opt,name=stdduration", - Filename: "gogo.proto", -} - -func init() { - proto.RegisterExtension(E_GoprotoEnumPrefix) - proto.RegisterExtension(E_GoprotoEnumStringer) - proto.RegisterExtension(E_EnumStringer) - proto.RegisterExtension(E_EnumCustomname) - proto.RegisterExtension(E_Enumdecl) - proto.RegisterExtension(E_EnumvalueCustomname) - proto.RegisterExtension(E_GoprotoGettersAll) - proto.RegisterExtension(E_GoprotoEnumPrefixAll) - proto.RegisterExtension(E_GoprotoStringerAll) - proto.RegisterExtension(E_VerboseEqualAll) - proto.RegisterExtension(E_FaceAll) - proto.RegisterExtension(E_GostringAll) - proto.RegisterExtension(E_PopulateAll) - proto.RegisterExtension(E_StringerAll) - proto.RegisterExtension(E_OnlyoneAll) - proto.RegisterExtension(E_EqualAll) - proto.RegisterExtension(E_DescriptionAll) - proto.RegisterExtension(E_TestgenAll) - proto.RegisterExtension(E_BenchgenAll) - proto.RegisterExtension(E_MarshalerAll) - proto.RegisterExtension(E_UnmarshalerAll) - proto.RegisterExtension(E_StableMarshalerAll) - proto.RegisterExtension(E_SizerAll) - proto.RegisterExtension(E_GoprotoEnumStringerAll) - proto.RegisterExtension(E_EnumStringerAll) - proto.RegisterExtension(E_UnsafeMarshalerAll) - proto.RegisterExtension(E_UnsafeUnmarshalerAll) - proto.RegisterExtension(E_GoprotoExtensionsMapAll) - proto.RegisterExtension(E_GoprotoUnrecognizedAll) - proto.RegisterExtension(E_GogoprotoImport) - proto.RegisterExtension(E_ProtosizerAll) - proto.RegisterExtension(E_CompareAll) - proto.RegisterExtension(E_TypedeclAll) - proto.RegisterExtension(E_EnumdeclAll) - proto.RegisterExtension(E_GoprotoRegistration) - proto.RegisterExtension(E_MessagenameAll) - proto.RegisterExtension(E_GoprotoGetters) - proto.RegisterExtension(E_GoprotoStringer) - proto.RegisterExtension(E_VerboseEqual) - proto.RegisterExtension(E_Face) - proto.RegisterExtension(E_Gostring) - proto.RegisterExtension(E_Populate) - proto.RegisterExtension(E_Stringer) - proto.RegisterExtension(E_Onlyone) - proto.RegisterExtension(E_Equal) - proto.RegisterExtension(E_Description) - proto.RegisterExtension(E_Testgen) - proto.RegisterExtension(E_Benchgen) - proto.RegisterExtension(E_Marshaler) - proto.RegisterExtension(E_Unmarshaler) - proto.RegisterExtension(E_StableMarshaler) - proto.RegisterExtension(E_Sizer) - proto.RegisterExtension(E_UnsafeMarshaler) - proto.RegisterExtension(E_UnsafeUnmarshaler) - proto.RegisterExtension(E_GoprotoExtensionsMap) - proto.RegisterExtension(E_GoprotoUnrecognized) - proto.RegisterExtension(E_Protosizer) - proto.RegisterExtension(E_Compare) - proto.RegisterExtension(E_Typedecl) - proto.RegisterExtension(E_Messagename) - proto.RegisterExtension(E_Nullable) - proto.RegisterExtension(E_Embed) - proto.RegisterExtension(E_Customtype) - proto.RegisterExtension(E_Customname) - proto.RegisterExtension(E_Jsontag) - proto.RegisterExtension(E_Moretags) - proto.RegisterExtension(E_Casttype) - proto.RegisterExtension(E_Castkey) - proto.RegisterExtension(E_Castvalue) - proto.RegisterExtension(E_Stdtime) - proto.RegisterExtension(E_Stdduration) -} - -func init() { proto.RegisterFile("gogo.proto", fileDescriptor_gogo_e935c22a8aa82c87) } - -var fileDescriptor_gogo_e935c22a8aa82c87 = []byte{ - // 1260 bytes of a gzipped FileDescriptorProto - 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0x94, 0x98, 0xc9, 0x6f, 0x1c, 0x45, - 0x17, 0xc0, 0xf5, 0xe9, 0x4b, 0x14, 0xcf, 0xf3, 0x86, 0xc7, 0xc6, 0x84, 0x08, 0x44, 0xb8, 0x71, - 0xc1, 0x73, 0x40, 0x11, 0x4a, 0x59, 0x91, 0xe5, 0x58, 0x8e, 0x15, 0x84, 0xc1, 0x98, 0xd8, 0x6c, - 0x87, 0x51, 0xcf, 0x4c, 0xb9, 0x33, 0xa4, 0xbb, 0xab, 0xe9, 0xae, 0x8e, 0xe2, 0xdc, 0x50, 0x58, - 0x84, 0x10, 0x3b, 0x12, 0x24, 0x24, 0x81, 0x1c, 0xd8, 0xd7, 0xb0, 0x73, 0xe3, 0xc2, 0x72, 0xe5, - 0x7f, 0xe0, 0x02, 0x98, 0xdd, 0x37, 0x5f, 0xd0, 0xeb, 0x7e, 0xaf, 0xa7, 0x66, 0x3c, 0x52, 0xd5, - 0xdc, 0xda, 0x76, 0xfd, 0x7e, 0xae, 0x7e, 0xaf, 0xea, 0xbd, 0x37, 0x03, 0xe0, 0x2b, 0x5f, 0xcd, - 0xc4, 0x89, 0xd2, 0xaa, 0x5a, 0xc1, 0xe7, 0xfc, 0xf1, 0xc0, 0x41, 0x5f, 0x29, 0x3f, 0x90, 0xb5, - 0xfc, 0xa7, 0x46, 0xb6, 0x51, 0x6b, 0xc9, 0xb4, 0x99, 0xb4, 0x63, 0xad, 0x92, 0x62, 0xb1, 0xb8, - 0x0b, 0x26, 0x69, 0x71, 0x5d, 0x46, 0x59, 0x58, 0x8f, 0x13, 0xb9, 0xd1, 0x3e, 0x53, 0xbd, 0x61, - 0xa6, 0x20, 0x67, 0x98, 0x9c, 0x59, 0x8c, 0xb2, 0xf0, 0xee, 0x58, 0xb7, 0x55, 0x94, 0xee, 0xbf, - 0xfa, 0xf3, 0xff, 0x0f, 0xfe, 0xef, 0x96, 0xa1, 0xd5, 0x09, 0x42, 0xf1, 0x6f, 0x2b, 0x39, 0x28, - 0x56, 0xe1, 0xda, 0x2e, 0x5f, 0xaa, 0x93, 0x76, 0xe4, 0xcb, 0xc4, 0x62, 0xfc, 0x8e, 0x8c, 0x93, - 0x86, 0xf1, 0x5e, 0x42, 0xc5, 0x02, 0x8c, 0x0e, 0xe2, 0xfa, 0x9e, 0x5c, 0x23, 0xd2, 0x94, 0x2c, - 0xc1, 0x78, 0x2e, 0x69, 0x66, 0xa9, 0x56, 0x61, 0xe4, 0x85, 0xd2, 0xa2, 0xf9, 0x21, 0xd7, 0x54, - 0x56, 0xc7, 0x10, 0x5b, 0x28, 0x29, 0x21, 0x60, 0x08, 0x7f, 0xd3, 0x92, 0xcd, 0xc0, 0x62, 0xf8, - 0x91, 0x36, 0x52, 0xae, 0x17, 0xeb, 0x30, 0x85, 0xcf, 0xa7, 0xbd, 0x20, 0x93, 0xe6, 0x4e, 0x6e, - 0xee, 0xeb, 0x59, 0xc7, 0x65, 0x2c, 0xfb, 0xe9, 0xdc, 0x9e, 0x7c, 0x3b, 0x93, 0xa5, 0xc0, 0xd8, - 0x93, 0x91, 0x45, 0x5f, 0x6a, 0x2d, 0x93, 0xb4, 0xee, 0x05, 0xfd, 0xb6, 0x77, 0xac, 0x1d, 0x94, - 0xc6, 0xf3, 0x5b, 0xdd, 0x59, 0x5c, 0x2a, 0xc8, 0xf9, 0x20, 0x10, 0x6b, 0x70, 0x5d, 0x9f, 0x53, - 0xe1, 0xe0, 0xbc, 0x40, 0xce, 0xa9, 0x5d, 0x27, 0x03, 0xb5, 0x2b, 0xc0, 0xbf, 0x2f, 0x73, 0xe9, - 0xe0, 0x7c, 0x8d, 0x9c, 0x55, 0x62, 0x39, 0xa5, 0x68, 0xbc, 0x03, 0x26, 0x4e, 0xcb, 0xa4, 0xa1, - 0x52, 0x59, 0x97, 0x8f, 0x64, 0x5e, 0xe0, 0xa0, 0xbb, 0x48, 0xba, 0x71, 0x02, 0x17, 0x91, 0x43, - 0xd7, 0x61, 0x18, 0xda, 0xf0, 0x9a, 0xd2, 0x41, 0x71, 0x89, 0x14, 0xfb, 0x70, 0x3d, 0xa2, 0xf3, - 0x30, 0xe2, 0xab, 0xe2, 0x95, 0x1c, 0xf0, 0xcb, 0x84, 0x0f, 0x33, 0x43, 0x8a, 0x58, 0xc5, 0x59, - 0xe0, 0x69, 0x97, 0x1d, 0xbc, 0xce, 0x0a, 0x66, 0x48, 0x31, 0x40, 0x58, 0xdf, 0x60, 0x45, 0x6a, - 0xc4, 0x73, 0x0e, 0x86, 0x55, 0x14, 0x6c, 0xaa, 0xc8, 0x65, 0x13, 0x57, 0xc8, 0x00, 0x84, 0xa0, - 0x60, 0x16, 0x2a, 0xae, 0x89, 0x78, 0x73, 0x8b, 0xaf, 0x07, 0x67, 0x60, 0x09, 0xc6, 0xb9, 0x40, - 0xb5, 0x55, 0xe4, 0xa0, 0x78, 0x8b, 0x14, 0x63, 0x06, 0x46, 0xaf, 0xa1, 0x65, 0xaa, 0x7d, 0xe9, - 0x22, 0x79, 0x9b, 0x5f, 0x83, 0x10, 0x0a, 0x65, 0x43, 0x46, 0xcd, 0x93, 0x6e, 0x86, 0x77, 0x38, - 0x94, 0xcc, 0xa0, 0x62, 0x01, 0x46, 0x43, 0x2f, 0x49, 0x4f, 0x7a, 0x81, 0x53, 0x3a, 0xde, 0x25, - 0xc7, 0x48, 0x09, 0x51, 0x44, 0xb2, 0x68, 0x10, 0xcd, 0x7b, 0x1c, 0x11, 0x03, 0xa3, 0xab, 0x97, - 0x6a, 0xaf, 0x11, 0xc8, 0xfa, 0x20, 0xb6, 0xf7, 0xf9, 0xea, 0x15, 0xec, 0xb2, 0x69, 0x9c, 0x85, - 0x4a, 0xda, 0x3e, 0xeb, 0xa4, 0xf9, 0x80, 0x33, 0x9d, 0x03, 0x08, 0x3f, 0x00, 0xd7, 0xf7, 0x6d, - 0x13, 0x0e, 0xb2, 0x0f, 0x49, 0x36, 0xdd, 0xa7, 0x55, 0x50, 0x49, 0x18, 0x54, 0xf9, 0x11, 0x97, - 0x04, 0xd9, 0xe3, 0x5a, 0x81, 0xa9, 0x2c, 0x4a, 0xbd, 0x8d, 0xc1, 0xa2, 0xf6, 0x31, 0x47, 0xad, - 0x60, 0xbb, 0xa2, 0x76, 0x02, 0xa6, 0xc9, 0x38, 0x58, 0x5e, 0x3f, 0xe1, 0xc2, 0x5a, 0xd0, 0x6b, - 0xdd, 0xd9, 0x7d, 0x08, 0x0e, 0x94, 0xe1, 0x3c, 0xa3, 0x65, 0x94, 0x22, 0x53, 0x0f, 0xbd, 0xd8, - 0xc1, 0x7c, 0x95, 0xcc, 0x5c, 0xf1, 0x17, 0x4b, 0xc1, 0xb2, 0x17, 0xa3, 0xfc, 0x7e, 0xd8, 0xcf, - 0xf2, 0x2c, 0x4a, 0x64, 0x53, 0xf9, 0x51, 0xfb, 0xac, 0x6c, 0x39, 0xa8, 0x3f, 0xed, 0x49, 0xd5, - 0x9a, 0x81, 0xa3, 0xf9, 0x38, 0x5c, 0x53, 0xce, 0x2a, 0xf5, 0x76, 0x18, 0xab, 0x44, 0x5b, 0x8c, - 0x9f, 0x71, 0xa6, 0x4a, 0xee, 0x78, 0x8e, 0x89, 0x45, 0x18, 0xcb, 0x7f, 0x74, 0x3d, 0x92, 0x9f, - 0x93, 0x68, 0xb4, 0x43, 0x51, 0xe1, 0x68, 0xaa, 0x30, 0xf6, 0x12, 0x97, 0xfa, 0xf7, 0x05, 0x17, - 0x0e, 0x42, 0xa8, 0x70, 0xe8, 0xcd, 0x58, 0x62, 0xb7, 0x77, 0x30, 0x7c, 0xc9, 0x85, 0x83, 0x19, - 0x52, 0xf0, 0xc0, 0xe0, 0xa0, 0xf8, 0x8a, 0x15, 0xcc, 0xa0, 0xe2, 0x9e, 0x4e, 0xa3, 0x4d, 0xa4, - 0xdf, 0x4e, 0x75, 0xe2, 0xe1, 0x6a, 0x8b, 0xea, 0xeb, 0xad, 0xee, 0x21, 0x6c, 0xd5, 0x40, 0xb1, - 0x12, 0x85, 0x32, 0x4d, 0x3d, 0x5f, 0xe2, 0xc4, 0xe1, 0xb0, 0xb1, 0x6f, 0xb8, 0x12, 0x19, 0x58, - 0x71, 0x3f, 0xc7, 0x7b, 0x66, 0x95, 0xea, 0x4d, 0xbb, 0x44, 0xcb, 0x05, 0xc3, 0xae, 0x47, 0xb7, - 0xc9, 0xd5, 0x3d, 0xaa, 0x88, 0x3b, 0xf1, 0x00, 0x75, 0x0f, 0x14, 0x76, 0xd9, 0xb9, 0xed, 0xf2, - 0x0c, 0x75, 0xcd, 0x13, 0xe2, 0x18, 0x8c, 0x76, 0x0d, 0x13, 0x76, 0xd5, 0x63, 0xa4, 0x1a, 0x31, - 0x67, 0x09, 0x71, 0x08, 0xf6, 0xe0, 0x60, 0x60, 0xc7, 0x1f, 0x27, 0x3c, 0x5f, 0x2e, 0x8e, 0xc0, - 0x10, 0x0f, 0x04, 0x76, 0xf4, 0x09, 0x42, 0x4b, 0x04, 0x71, 0x1e, 0x06, 0xec, 0xf8, 0x93, 0x8c, - 0x33, 0x82, 0xb8, 0x7b, 0x08, 0xbf, 0x7d, 0x7a, 0x0f, 0x15, 0x74, 0x8e, 0xdd, 0x2c, 0xec, 0xa3, - 0x29, 0xc0, 0x4e, 0x3f, 0x45, 0xff, 0x9c, 0x09, 0x71, 0x3b, 0xec, 0x75, 0x0c, 0xf8, 0x33, 0x84, - 0x16, 0xeb, 0xc5, 0x02, 0x0c, 0x1b, 0x9d, 0xdf, 0x8e, 0x3f, 0x4b, 0xb8, 0x49, 0xe1, 0xd6, 0xa9, - 0xf3, 0xdb, 0x05, 0xcf, 0xf1, 0xd6, 0x89, 0xc0, 0xb0, 0x71, 0xd3, 0xb7, 0xd3, 0xcf, 0x73, 0xd4, - 0x19, 0x11, 0x73, 0x50, 0x29, 0x0b, 0xb9, 0x9d, 0x7f, 0x81, 0xf8, 0x0e, 0x83, 0x11, 0x30, 0x1a, - 0x89, 0x5d, 0xf1, 0x22, 0x47, 0xc0, 0xa0, 0xf0, 0x1a, 0xf5, 0x0e, 0x07, 0x76, 0xd3, 0x4b, 0x7c, - 0x8d, 0x7a, 0x66, 0x03, 0xcc, 0x66, 0x5e, 0x4f, 0xed, 0x8a, 0x97, 0x39, 0x9b, 0xf9, 0x7a, 0xdc, - 0x46, 0x6f, 0xb7, 0xb5, 0x3b, 0x5e, 0xe1, 0x6d, 0xf4, 0x34, 0x5b, 0xb1, 0x02, 0xd5, 0xdd, 0x9d, - 0xd6, 0xee, 0x7b, 0x95, 0x7c, 0x13, 0xbb, 0x1a, 0xad, 0xb8, 0x0f, 0xa6, 0xfb, 0x77, 0x59, 0xbb, - 0xf5, 0xfc, 0x76, 0xcf, 0xe7, 0x22, 0xb3, 0xc9, 0x8a, 0x13, 0x9d, 0x72, 0x6d, 0x76, 0x58, 0xbb, - 0xf6, 0xc2, 0x76, 0x77, 0xc5, 0x36, 0x1b, 0xac, 0x98, 0x07, 0xe8, 0x34, 0x37, 0xbb, 0xeb, 0x22, - 0xb9, 0x0c, 0x08, 0xaf, 0x06, 0xf5, 0x36, 0x3b, 0x7f, 0x89, 0xaf, 0x06, 0x11, 0x78, 0x35, 0xb8, - 0xad, 0xd9, 0xe9, 0xcb, 0x7c, 0x35, 0x18, 0xc1, 0x93, 0x6d, 0x74, 0x0e, 0xbb, 0xe1, 0x0a, 0x9f, - 0x6c, 0x83, 0x12, 0xb3, 0x30, 0x14, 0x65, 0x41, 0x80, 0x07, 0xb4, 0x7a, 0x63, 0x9f, 0x76, 0x25, - 0x83, 0x16, 0xf3, 0xbf, 0xec, 0xd0, 0x0e, 0x18, 0x10, 0x87, 0x60, 0xaf, 0x0c, 0x1b, 0xb2, 0x65, - 0x23, 0x7f, 0xdd, 0xe1, 0xa2, 0x84, 0xab, 0xc5, 0x1c, 0x40, 0xf1, 0xd1, 0x1e, 0x5f, 0xc5, 0xc6, - 0xfe, 0xb6, 0x53, 0x7c, 0xcb, 0x60, 0x20, 0x1d, 0x41, 0xfe, 0xe2, 0x16, 0xc1, 0x56, 0xb7, 0x20, - 0x7f, 0xeb, 0xc3, 0xb0, 0xef, 0xe1, 0x54, 0x45, 0xda, 0xf3, 0x6d, 0xf4, 0xef, 0x44, 0xf3, 0x7a, - 0x0c, 0x58, 0xa8, 0x12, 0xa9, 0x3d, 0x3f, 0xb5, 0xb1, 0x7f, 0x10, 0x5b, 0x02, 0x08, 0x37, 0xbd, - 0x54, 0xbb, 0xbc, 0xf7, 0x9f, 0x0c, 0x33, 0x80, 0x9b, 0xc6, 0xe7, 0x53, 0x72, 0xd3, 0xc6, 0xfe, - 0xc5, 0x9b, 0xa6, 0xf5, 0xe2, 0x08, 0x54, 0xf0, 0x31, 0xff, 0x56, 0xc4, 0x06, 0xff, 0x4d, 0x70, - 0x87, 0xc0, 0xff, 0x9c, 0xea, 0x96, 0x6e, 0xdb, 0x83, 0xfd, 0x0f, 0x65, 0x9a, 0xd7, 0x8b, 0x79, - 0x18, 0x4e, 0x75, 0xab, 0x95, 0xd1, 0x7c, 0x65, 0xc1, 0xff, 0xdd, 0x29, 0x3f, 0x72, 0x97, 0xcc, - 0xd1, 0x75, 0x98, 0x6c, 0xaa, 0xb0, 0x17, 0x3c, 0x0a, 0x4b, 0x6a, 0x49, 0xad, 0xe4, 0x57, 0xf1, - 0xc1, 0xdb, 0x7c, 0x75, 0x6b, 0x53, 0x85, 0xa1, 0x8a, 0x6a, 0x5e, 0x1c, 0xd7, 0xb4, 0x52, 0x41, - 0xad, 0x11, 0xe6, 0x4b, 0x6b, 0xf1, 0x29, 0xbf, 0xd6, 0xa9, 0x46, 0xb5, 0x72, 0x2e, 0xfe, 0x2f, - 0x00, 0x00, 0xff, 0xff, 0x97, 0xb1, 0x98, 0x88, 0x13, 0x14, 0x00, 0x00, -} diff --git a/tool/protobuf/pkg/extensions/gogoproto/gogo.pb.golden b/tool/protobuf/pkg/extensions/gogoproto/gogo.pb.golden deleted file mode 100644 index f6502e4b9..000000000 --- a/tool/protobuf/pkg/extensions/gogoproto/gogo.pb.golden +++ /dev/null @@ -1,45 +0,0 @@ -// Code generated by protoc-gen-go. -// source: gogo.proto -// DO NOT EDIT! - -package gogoproto - -import proto "github.com/gogo/protobuf/proto" -import json "encoding/json" -import math "math" -import google_protobuf "github.com/gogo/protobuf/protoc-gen-gogo/descriptor" - -// Reference proto, json, and math imports to suppress error if they are not otherwise used. -var _ = proto.Marshal -var _ = &json.SyntaxError{} -var _ = math.Inf - -var E_Nullable = &proto.ExtensionDesc{ - ExtendedType: (*google_protobuf.FieldOptions)(nil), - ExtensionType: (*bool)(nil), - Field: 51235, - Name: "gogoproto.nullable", - Tag: "varint,51235,opt,name=nullable", -} - -var E_Embed = &proto.ExtensionDesc{ - ExtendedType: (*google_protobuf.FieldOptions)(nil), - ExtensionType: (*bool)(nil), - Field: 51236, - Name: "gogoproto.embed", - Tag: "varint,51236,opt,name=embed", -} - -var E_Customtype = &proto.ExtensionDesc{ - ExtendedType: (*google_protobuf.FieldOptions)(nil), - ExtensionType: (*string)(nil), - Field: 51237, - Name: "gogoproto.customtype", - Tag: "bytes,51237,opt,name=customtype", -} - -func init() { - proto.RegisterExtension(E_Nullable) - proto.RegisterExtension(E_Embed) - proto.RegisterExtension(E_Customtype) -} diff --git a/tool/protobuf/pkg/extensions/gogoproto/gogo.proto b/tool/protobuf/pkg/extensions/gogoproto/gogo.proto deleted file mode 100644 index d7a8b3709..000000000 --- a/tool/protobuf/pkg/extensions/gogoproto/gogo.proto +++ /dev/null @@ -1,136 +0,0 @@ -// Protocol Buffers for Go with Gadgets -// -// Copyright (c) 2013, The GoGo Authors. All rights reserved. -// http://github.com/gogo/protobuf -// -// Redistribution and use in source and binary forms, with or without -// modification, are permitted provided that the following conditions are -// met: -// -// * Redistributions of source code must retain the above copyright -// notice, this list of conditions and the following disclaimer. -// * Redistributions in binary form must reproduce the above -// copyright notice, this list of conditions and the following disclaimer -// in the documentation and/or other materials provided with the -// distribution. -// -// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS -// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT -// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR -// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT -// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, -// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY -// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT -// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE -// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - -syntax = "proto2"; -package gogoproto; - -import "google/protobuf/descriptor.proto"; - -option java_package = "com.google.protobuf"; -option java_outer_classname = "GoGoProtos"; -option go_package = "github.com/go-kratos/kratos/tool/protobuf/pkg/extensions/gogoproto"; - -extend google.protobuf.EnumOptions { - optional bool goproto_enum_prefix = 62001; - optional bool goproto_enum_stringer = 62021; - optional bool enum_stringer = 62022; - optional string enum_customname = 62023; - optional bool enumdecl = 62024; -} - -extend google.protobuf.EnumValueOptions { - optional string enumvalue_customname = 66001; -} - -extend google.protobuf.FileOptions { - optional bool goproto_getters_all = 63001; - optional bool goproto_enum_prefix_all = 63002; - optional bool goproto_stringer_all = 63003; - optional bool verbose_equal_all = 63004; - optional bool face_all = 63005; - optional bool gostring_all = 63006; - optional bool populate_all = 63007; - optional bool stringer_all = 63008; - optional bool onlyone_all = 63009; - - optional bool equal_all = 63013; - optional bool description_all = 63014; - optional bool testgen_all = 63015; - optional bool benchgen_all = 63016; - optional bool marshaler_all = 63017; - optional bool unmarshaler_all = 63018; - optional bool stable_marshaler_all = 63019; - - optional bool sizer_all = 63020; - - optional bool goproto_enum_stringer_all = 63021; - optional bool enum_stringer_all = 63022; - - optional bool unsafe_marshaler_all = 63023; - optional bool unsafe_unmarshaler_all = 63024; - - optional bool goproto_extensions_map_all = 63025; - optional bool goproto_unrecognized_all = 63026; - optional bool gogoproto_import = 63027; - optional bool protosizer_all = 63028; - optional bool compare_all = 63029; - optional bool typedecl_all = 63030; - optional bool enumdecl_all = 63031; - - optional bool goproto_registration = 63032; - optional bool messagename_all = 63033; -} - -extend google.protobuf.MessageOptions { - optional bool goproto_getters = 64001; - optional bool goproto_stringer = 64003; - optional bool verbose_equal = 64004; - optional bool face = 64005; - optional bool gostring = 64006; - optional bool populate = 64007; - optional bool stringer = 67008; - optional bool onlyone = 64009; - - optional bool equal = 64013; - optional bool description = 64014; - optional bool testgen = 64015; - optional bool benchgen = 64016; - optional bool marshaler = 64017; - optional bool unmarshaler = 64018; - optional bool stable_marshaler = 64019; - - optional bool sizer = 64020; - - optional bool unsafe_marshaler = 64023; - optional bool unsafe_unmarshaler = 64024; - - optional bool goproto_extensions_map = 64025; - optional bool goproto_unrecognized = 64026; - - optional bool protosizer = 64028; - optional bool compare = 64029; - - optional bool typedecl = 64030; - - optional bool messagename = 64033; -} - -extend google.protobuf.FieldOptions { - optional bool nullable = 65001; - optional bool embed = 65002; - optional string customtype = 65003; - optional string customname = 65004; - optional string jsontag = 65005; - optional string moretags = 65006; - optional string casttype = 65007; - optional string castkey = 65008; - optional string castvalue = 65009; - - optional bool stdtime = 65010; - optional bool stdduration = 65011; -} diff --git a/tool/protobuf/pkg/extensions/google/api/annotations.proto b/tool/protobuf/pkg/extensions/google/api/annotations.proto deleted file mode 100644 index 85c361b47..000000000 --- a/tool/protobuf/pkg/extensions/google/api/annotations.proto +++ /dev/null @@ -1,31 +0,0 @@ -// Copyright (c) 2015, Google Inc. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -syntax = "proto3"; - -package google.api; - -import "google/api/http.proto"; -import "google/protobuf/descriptor.proto"; - -option go_package = "google.golang.org/genproto/googleapis/api/annotations;annotations"; -option java_multiple_files = true; -option java_outer_classname = "AnnotationsProto"; -option java_package = "com.google.api"; -option objc_class_prefix = "GAPI"; - -extend google.protobuf.MethodOptions { - // See `HttpRule`. - HttpRule http = 72295728; -} diff --git a/tool/protobuf/pkg/extensions/google/api/http.proto b/tool/protobuf/pkg/extensions/google/api/http.proto deleted file mode 100644 index 2bd3a19bf..000000000 --- a/tool/protobuf/pkg/extensions/google/api/http.proto +++ /dev/null @@ -1,318 +0,0 @@ -// Copyright 2018 Google LLC -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -syntax = "proto3"; - -package google.api; - -option cc_enable_arenas = true; -option go_package = "google.golang.org/genproto/googleapis/api/annotations;annotations"; -option java_multiple_files = true; -option java_outer_classname = "HttpProto"; -option java_package = "com.google.api"; -option objc_class_prefix = "GAPI"; - - -// Defines the HTTP configuration for an API service. It contains a list of -// [HttpRule][google.api.HttpRule], each specifying the mapping of an RPC method -// to one or more HTTP REST API methods. -message Http { - // A list of HTTP configuration rules that apply to individual API methods. - // - // **NOTE:** All service configuration rules follow "last one wins" order. - repeated HttpRule rules = 1; - - // When set to true, URL path parmeters will be fully URI-decoded except in - // cases of single segment matches in reserved expansion, where "%2F" will be - // left encoded. - // - // The default behavior is to not decode RFC 6570 reserved characters in multi - // segment matches. - bool fully_decode_reserved_expansion = 2; -} - -// `HttpRule` defines the mapping of an RPC method to one or more HTTP -// REST API methods. The mapping specifies how different portions of the RPC -// request message are mapped to URL path, URL query parameters, and -// HTTP request body. The mapping is typically specified as an -// `google.api.http` annotation on the RPC method, -// see "google/api/annotations.proto" for details. -// -// The mapping consists of a field specifying the path template and -// method kind. The path template can refer to fields in the request -// message, as in the example below which describes a REST GET -// operation on a resource collection of messages: -// -// -// service Messaging { -// rpc GetMessage(GetMessageRequest) returns (Message) { -// option (google.api.http).get = "/v1/messages/{message_id}/{sub.subfield}"; -// } -// } -// message GetMessageRequest { -// message SubMessage { -// string subfield = 1; -// } -// string message_id = 1; // mapped to the URL -// SubMessage sub = 2; // `sub.subfield` is url-mapped -// } -// message Message { -// string text = 1; // content of the resource -// } -// -// The same http annotation can alternatively be expressed inside the -// `GRPC API Configuration` YAML file. -// -// http: -// rules: -// - selector: .Messaging.GetMessage -// get: /v1/messages/{message_id}/{sub.subfield} -// -// This definition enables an automatic, bidrectional mapping of HTTP -// JSON to RPC. Example: -// -// HTTP | RPC -// -----|----- -// `GET /v1/messages/123456/foo` | `GetMessage(message_id: "123456" sub: SubMessage(subfield: "foo"))` -// -// In general, not only fields but also field paths can be referenced -// from a path pattern. Fields mapped to the path pattern cannot be -// repeated and must have a primitive (non-message) type. -// -// Any fields in the request message which are not bound by the path -// pattern automatically become (optional) HTTP query -// parameters. Assume the following definition of the request message: -// -// -// service Messaging { -// rpc GetMessage(GetMessageRequest) returns (Message) { -// option (google.api.http).get = "/v1/messages/{message_id}"; -// } -// } -// message GetMessageRequest { -// message SubMessage { -// string subfield = 1; -// } -// string message_id = 1; // mapped to the URL -// int64 revision = 2; // becomes a parameter -// SubMessage sub = 3; // `sub.subfield` becomes a parameter -// } -// -// -// This enables a HTTP JSON to RPC mapping as below: -// -// HTTP | RPC -// -----|----- -// `GET /v1/messages/123456?revision=2&sub.subfield=foo` | `GetMessage(message_id: "123456" revision: 2 sub: SubMessage(subfield: "foo"))` -// -// Note that fields which are mapped to HTTP parameters must have a -// primitive type or a repeated primitive type. Message types are not -// allowed. In the case of a repeated type, the parameter can be -// repeated in the URL, as in `...?param=A¶m=B`. -// -// For HTTP method kinds which allow a request body, the `body` field -// specifies the mapping. Consider a REST update method on the -// message resource collection: -// -// -// service Messaging { -// rpc UpdateMessage(UpdateMessageRequest) returns (Message) { -// option (google.api.http) = { -// put: "/v1/messages/{message_id}" -// body: "message" -// }; -// } -// } -// message UpdateMessageRequest { -// string message_id = 1; // mapped to the URL -// Message message = 2; // mapped to the body -// } -// -// -// The following HTTP JSON to RPC mapping is enabled, where the -// representation of the JSON in the request body is determined by -// protos JSON encoding: -// -// HTTP | RPC -// -----|----- -// `PUT /v1/messages/123456 { "text": "Hi!" }` | `UpdateMessage(message_id: "123456" message { text: "Hi!" })` -// -// The special name `*` can be used in the body mapping to define that -// every field not bound by the path template should be mapped to the -// request body. This enables the following alternative definition of -// the update method: -// -// service Messaging { -// rpc UpdateMessage(Message) returns (Message) { -// option (google.api.http) = { -// put: "/v1/messages/{message_id}" -// body: "*" -// }; -// } -// } -// message Message { -// string message_id = 1; -// string text = 2; -// } -// -// -// The following HTTP JSON to RPC mapping is enabled: -// -// HTTP | RPC -// -----|----- -// `PUT /v1/messages/123456 { "text": "Hi!" }` | `UpdateMessage(message_id: "123456" text: "Hi!")` -// -// Note that when using `*` in the body mapping, it is not possible to -// have HTTP parameters, as all fields not bound by the path end in -// the body. This makes this option more rarely used in practice of -// defining REST APIs. The common usage of `*` is in custom methods -// which don't use the URL at all for transferring data. -// -// It is possible to define multiple HTTP methods for one RPC by using -// the `additional_bindings` option. Example: -// -// service Messaging { -// rpc GetMessage(GetMessageRequest) returns (Message) { -// option (google.api.http) = { -// get: "/v1/messages/{message_id}" -// additional_bindings { -// get: "/v1/users/{user_id}/messages/{message_id}" -// } -// }; -// } -// } -// message GetMessageRequest { -// string message_id = 1; -// string user_id = 2; -// } -// -// -// This enables the following two alternative HTTP JSON to RPC -// mappings: -// -// HTTP | RPC -// -----|----- -// `GET /v1/messages/123456` | `GetMessage(message_id: "123456")` -// `GET /v1/users/me/messages/123456` | `GetMessage(user_id: "me" message_id: "123456")` -// -// # Rules for HTTP mapping -// -// The rules for mapping HTTP path, query parameters, and body fields -// to the request message are as follows: -// -// 1. The `body` field specifies either `*` or a field path, or is -// omitted. If omitted, it indicates there is no HTTP request body. -// 2. Leaf fields (recursive expansion of nested messages in the -// request) can be classified into three types: -// (a) Matched in the URL template. -// (b) Covered by body (if body is `*`, everything except (a) fields; -// else everything under the body field) -// (c) All other fields. -// 3. URL query parameters found in the HTTP request are mapped to (c) fields. -// 4. Any body sent with an HTTP request can contain only (b) fields. -// -// The syntax of the path template is as follows: -// -// Template = "/" Segments [ Verb ] ; -// Segments = Segment { "/" Segment } ; -// Segment = "*" | "**" | LITERAL | Variable ; -// Variable = "{" FieldPath [ "=" Segments ] "}" ; -// FieldPath = IDENT { "." IDENT } ; -// Verb = ":" LITERAL ; -// -// The syntax `*` matches a single path segment. The syntax `**` matches zero -// or more path segments, which must be the last part of the path except the -// `Verb`. The syntax `LITERAL` matches literal text in the path. -// -// The syntax `Variable` matches part of the URL path as specified by its -// template. A variable template must not contain other variables. If a variable -// matches a single path segment, its template may be omitted, e.g. `{var}` -// is equivalent to `{var=*}`. -// -// If a variable contains exactly one path segment, such as `"{var}"` or -// `"{var=*}"`, when such a variable is expanded into a URL path, all characters -// except `[-_.~0-9a-zA-Z]` are percent-encoded. Such variables show up in the -// Discovery Document as `{var}`. -// -// If a variable contains one or more path segments, such as `"{var=foo/*}"` -// or `"{var=**}"`, when such a variable is expanded into a URL path, all -// characters except `[-_.~/0-9a-zA-Z]` are percent-encoded. Such variables -// show up in the Discovery Document as `{+var}`. -// -// NOTE: While the single segment variable matches the semantics of -// [RFC 6570](https://tools.ietf.org/html/rfc6570) Section 3.2.2 -// Simple String Expansion, the multi segment variable **does not** match -// RFC 6570 Reserved Expansion. The reason is that the Reserved Expansion -// does not expand special characters like `?` and `#`, which would lead -// to invalid URLs. -// -// NOTE: the field paths in variables and in the `body` must not refer to -// repeated fields or map fields. -message HttpRule { - // Selects methods to which this rule applies. - // - // Refer to [selector][google.api.DocumentationRule.selector] for syntax details. - string selector = 1; - - // Determines the URL pattern is matched by this rules. This pattern can be - // used with any of the {get|put|post|delete|patch} methods. A custom method - // can be defined using the 'custom' field. - oneof pattern { - // Used for listing and getting information about resources. - string get = 2; - - // Used for updating a resource. - string put = 3; - - // Used for creating a resource. - string post = 4; - - // Used for deleting a resource. - string delete = 5; - - // Used for updating a resource. - string patch = 6; - - // The custom pattern is used for specifying an HTTP method that is not - // included in the `pattern` field, such as HEAD, or "*" to leave the - // HTTP method unspecified for this rule. The wild-card rule is useful - // for services that provide content to Web (HTML) clients. - CustomHttpPattern custom = 8; - } - - // The name of the request field whose value is mapped to the HTTP body, or - // `*` for mapping all fields not captured by the path pattern to the HTTP - // body. NOTE: the referred field must not be a repeated field and must be - // present at the top-level of request message type. - string body = 7; - - // Optional. The name of the response field whose value is mapped to the HTTP - // body of response. Other response fields are ignored. When - // not set, the response message will be used as HTTP body of response. - string response_body = 12; - - // Additional HTTP bindings for the selector. Nested bindings must - // not contain an `additional_bindings` field themselves (that is, - // the nesting may only be one level deep). - repeated HttpRule additional_bindings = 11; -} - -// A custom pattern is used for defining custom HTTP verb. -message CustomHttpPattern { - // The name of this custom HTTP verb. - string kind = 1; - - // The path matched by this custom verb. - string path = 2; -} diff --git a/tool/protobuf/pkg/gen/main.go b/tool/protobuf/pkg/gen/main.go deleted file mode 100644 index 071ba2ae2..000000000 --- a/tool/protobuf/pkg/gen/main.go +++ /dev/null @@ -1,92 +0,0 @@ -package gen - -import ( - "io" - "io/ioutil" - "log" - "os" - "strings" - - "github.com/golang/protobuf/proto" - "github.com/golang/protobuf/protoc-gen-go/descriptor" - plugin "github.com/golang/protobuf/protoc-gen-go/plugin" -) - -// Generator ... -type Generator interface { - Generate(in *plugin.CodeGeneratorRequest) *plugin.CodeGeneratorResponse -} - -// Main ... -func Main(g Generator) { - req := readGenRequest() - resp := g.Generate(req) - writeResponse(os.Stdout, resp) -} - -// FilesToGenerate ... -func FilesToGenerate(req *plugin.CodeGeneratorRequest) []*descriptor.FileDescriptorProto { - genFiles := make([]*descriptor.FileDescriptorProto, 0) -Outer: - for _, name := range req.FileToGenerate { - for _, f := range req.ProtoFile { - if f.GetName() == name { - genFiles = append(genFiles, f) - continue Outer - } - } - Fail("could not find file named", name) - } - - return genFiles -} - -func readGenRequest() *plugin.CodeGeneratorRequest { - data, err := ioutil.ReadAll(os.Stdin) - if err != nil { - Error(err, "reading input") - } - - req := new(plugin.CodeGeneratorRequest) - if err = proto.Unmarshal(data, req); err != nil { - Error(err, "parsing input proto") - } - - if len(req.FileToGenerate) == 0 { - Fail("no files to generate") - } - - return req -} - -func writeResponse(w io.Writer, resp *plugin.CodeGeneratorResponse) { - data, err := proto.Marshal(resp) - if err != nil { - Error(err, "marshaling response") - } - _, err = w.Write(data) - if err != nil { - Error(err, "writing response") - } -} - -// Fail log and exit -func Fail(msgs ...string) { - s := strings.Join(msgs, " ") - log.Print("error:", s) - os.Exit(1) -} - -// Fail log and exit -func Info(msgs ...string) { - s := strings.Join(msgs, " ") - log.Print("info:", s) - os.Exit(1) -} - -// Error log and exit -func Error(err error, msgs ...string) { - s := strings.Join(msgs, " ") + ":" + err.Error() - log.Print("error:", s) - os.Exit(1) -} diff --git a/tool/protobuf/pkg/generator/command_line.go b/tool/protobuf/pkg/generator/command_line.go deleted file mode 100644 index 551a558de..000000000 --- a/tool/protobuf/pkg/generator/command_line.go +++ /dev/null @@ -1,71 +0,0 @@ -package generator - -import ( - "fmt" - "strings" -) - -type ParamsBase struct { - ImportPrefix string // String to prefix to imported package file names. - ImportMap map[string]string // Mapping from .proto file name to import path. - //Tpl bool // generate service implementation template - ExplicitHTTP bool // Only generate for method that add http option -} - -type GeneratorParamsInterface interface { - GetBase() *ParamsBase - SetParam(key string, value string) error -} - -type BasicParam struct{ ParamsBase } - -func (b *BasicParam) GetBase() *ParamsBase { - return &b.ParamsBase -} -func (b *BasicParam) SetParam(key string, value string) error { - return nil -} - -func ParseGeneratorParams(parameter string, result GeneratorParamsInterface) error { - ps := make(map[string]string) - for _, p := range strings.Split(parameter, ",") { - if p == "" { - continue - } - i := strings.Index(p, "=") - if i < 0 { - return fmt.Errorf("invalid parameter %q: expected format of parameter to be k=v", p) - } - k := p[0:i] - v := p[i+1:] - if v == "" { - return fmt.Errorf("invalid parameter %q: expected format of parameter to be k=v", k) - } - ps[k] = v - } - - if result.GetBase().ImportMap == nil { - result.GetBase().ImportMap = map[string]string{} - } - for k, v := range ps { - switch { - case k == "explicit_http": - if v == "true" || v == "1" { - result.GetBase().ExplicitHTTP = true - } - case k == "import_prefix": - result.GetBase().ImportPrefix = v - // Support import map 'M' prefix per https://github.com/golang/protobuf/blob/6fb5325/protoc-gen-go/generator/generator.go#L497. - case len(k) > 0 && k[0] == 'M': - result.GetBase().ImportMap[k[1:]] = v // 1 is the length of 'M'. - case len(k) > 0 && strings.HasPrefix(k, "go_import_mapping@"): - result.GetBase().ImportMap[k[18:]] = v // 18 is the length of 'go_import_mapping@'. - default: - e := result.SetParam(k, v) - if e != nil { - return e - } - } - } - return nil -} diff --git a/tool/protobuf/pkg/generator/generator.go b/tool/protobuf/pkg/generator/generator.go deleted file mode 100644 index 3a228053d..000000000 --- a/tool/protobuf/pkg/generator/generator.go +++ /dev/null @@ -1,318 +0,0 @@ -package generator - -import ( - "bufio" - "bytes" - "fmt" - "go/parser" - "go/printer" - "go/token" - "path" - "strconv" - "strings" - - "github.com/golang/protobuf/protoc-gen-go/descriptor" - plugin "github.com/golang/protobuf/protoc-gen-go/plugin" - "github.com/pkg/errors" - - "github.com/go-kratos/kratos/tool/protobuf/pkg/gen" - "github.com/go-kratos/kratos/tool/protobuf/pkg/naming" - "github.com/go-kratos/kratos/tool/protobuf/pkg/typemap" - "github.com/go-kratos/kratos/tool/protobuf/pkg/utils" -) - -const Version = "v0.1" - -var GoModuleImportPath = "github.com/go-kratos/kratos" -var GoModuleDirName = "github.com/go-kratos/kratos" - -type Base struct { - Reg *typemap.Registry - - // Map to record whether we've built each package - // pkgName => alias name - pkgs map[string]string - pkgNamesInUse map[string]bool - - ImportPrefix string // String to prefix to imported package file names. - importMap map[string]string // Mapping from .proto file name to import path. - - // Package naming: - GenPkgName string // Name of the package that we're generating - PackageName string // Name of the proto file package - fileToGoPackageName map[*descriptor.FileDescriptorProto]string - - // List of files that were inputs to the generator. We need to hold this in - // the struct so we can write a header for the file that lists its inputs. - GenFiles []*descriptor.FileDescriptorProto - - // Output buffer that holds the bytes we want to write out for a single file. - // Gets reset after working on a file. - Output *bytes.Buffer - - // key: pkgName - // value: importPath - Deps map[string]string - - Params *ParamsBase - - httpInfoCache map[string]*HTTPInfo -} - -// RegisterPackageName name is the go package name or proto pkg name -// return go pkg alias -func (t *Base) RegisterPackageName(name string) (alias string) { - alias = name - i := 1 - for t.pkgNamesInUse[alias] { - alias = name + strconv.Itoa(i) - i++ - } - t.pkgNamesInUse[alias] = true - t.pkgs[name] = alias - return alias -} - -func (t *Base) Setup(in *plugin.CodeGeneratorRequest, paramsOpt ...GeneratorParamsInterface) { - t.httpInfoCache = make(map[string]*HTTPInfo) - t.pkgs = make(map[string]string) - t.pkgNamesInUse = make(map[string]bool) - t.importMap = make(map[string]string) - t.Deps = make(map[string]string) - t.fileToGoPackageName = make(map[*descriptor.FileDescriptorProto]string) - t.Output = bytes.NewBuffer(nil) - - var params GeneratorParamsInterface - if len(paramsOpt) > 0 { - params = paramsOpt[0] - } else { - params = &BasicParam{} - } - err := ParseGeneratorParams(in.GetParameter(), params) - if err != nil { - gen.Fail("could not parse parameters", err.Error()) - } - t.Params = params.GetBase() - t.ImportPrefix = params.GetBase().ImportPrefix - t.importMap = params.GetBase().ImportMap - - t.GenFiles = gen.FilesToGenerate(in) - - // Collect information on types. - t.Reg = typemap.New(in.ProtoFile) - t.RegisterPackageName("context") - t.RegisterPackageName("ioutil") - t.RegisterPackageName("proto") - // Time to figure out package names of objects defined in protobuf. First, - // we'll figure out the name for the package we're generating. - genPkgName, err := DeduceGenPkgName(t.GenFiles) - if err != nil { - gen.Fail(err.Error()) - } - t.GenPkgName = genPkgName - // Next, we need to pick names for all the files that are dependencies. - if len(in.ProtoFile) > 0 { - t.PackageName = t.GenFiles[0].GetPackage() - } - - for _, f := range in.ProtoFile { - if fileDescSliceContains(t.GenFiles, f) { - // This is a file we are generating. It gets the shared package name. - t.fileToGoPackageName[f] = t.GenPkgName - } else { - // This is a dependency. Use its package name. - name := f.GetPackage() - if name == "" { - name = utils.BaseName(f.GetName()) - } - name = utils.CleanIdentifier(name) - alias := t.RegisterPackageName(name) - t.fileToGoPackageName[f] = alias - } - } - - for _, f := range t.GenFiles { - deps := t.DeduceDeps(f) - for k, v := range deps { - t.Deps[k] = v - } - } -} - -func (t *Base) DeduceDeps(file *descriptor.FileDescriptorProto) map[string]string { - deps := make(map[string]string) // Map of package name to quoted import path. - ourImportPath := path.Dir(naming.GoFileName(file, "")) - for _, s := range file.Service { - for _, m := range s.Method { - defs := []*typemap.MessageDefinition{ - t.Reg.MethodInputDefinition(m), - t.Reg.MethodOutputDefinition(m), - } - for _, def := range defs { - if def.File.GetPackage() == t.PackageName { - continue - } - // By default, import path is the dirname of the Go filename. - importPath := path.Dir(naming.GoFileName(def.File, "")) - if importPath == ourImportPath { - continue - } - importPath = t.SubstituteImportPath(importPath, def.File.GetName()) - importPath = t.ImportPrefix + importPath - pkg := t.GoPackageNameForProtoFile(def.File) - deps[pkg] = strconv.Quote(importPath) - } - } - } - return deps -} - -// DeduceGenPkgName figures out the go package name to use for generated code. -// Will try to use the explicit go_package setting in a file (if set, must be -// consistent in all files). If no files have go_package set, then use the -// protobuf package name (must be consistent in all files) -func DeduceGenPkgName(genFiles []*descriptor.FileDescriptorProto) (string, error) { - var genPkgName string - for _, f := range genFiles { - name, explicit := naming.GoPackageName(f) - if explicit { - name = utils.CleanIdentifier(name) - if genPkgName != "" && genPkgName != name { - // Make sure they're all set consistently. - return "", errors.Errorf("files have conflicting go_package settings, must be the same: %q and %q", genPkgName, name) - } - genPkgName = name - } - } - if genPkgName != "" { - return genPkgName, nil - } - - // If there is no explicit setting, then check the implicit package name - // (derived from the protobuf package name) of the files and make sure it's - // consistent. - for _, f := range genFiles { - name, _ := naming.GoPackageName(f) - name = utils.CleanIdentifier(name) - if genPkgName != "" && genPkgName != name { - return "", errors.Errorf("files have conflicting package names, must be the same or overridden with go_package: %q and %q", genPkgName, name) - } - genPkgName = name - } - - // All the files have the same name, so we're good. - return genPkgName, nil -} - -func (t *Base) GoPackageNameForProtoFile(file *descriptor.FileDescriptorProto) string { - return t.fileToGoPackageName[file] -} - -func fileDescSliceContains(slice []*descriptor.FileDescriptorProto, f *descriptor.FileDescriptorProto) bool { - for _, sf := range slice { - if f == sf { - return true - } - } - return false -} - -// P forwards to g.gen.P, which prints output. -func (t *Base) P(args ...string) { - for _, v := range args { - t.Output.WriteString(v) - } - t.Output.WriteByte('\n') -} - -func (t *Base) FormattedOutput() string { - // Reformat generated code. - fset := token.NewFileSet() - raw := t.Output.Bytes() - ast, err := parser.ParseFile(fset, "", raw, parser.ParseComments) - if err != nil { - // Print out the bad code with line numbers. - // This should never happen in practice, but it can while changing generated code, - // so consider this a debugging aid. - var src bytes.Buffer - s := bufio.NewScanner(bytes.NewReader(raw)) - for line := 1; s.Scan(); line++ { - fmt.Fprintf(&src, "%5d\t%s\n", line, s.Bytes()) - } - gen.Fail("bad Go source code was generated:", err.Error(), "\n"+src.String()) - } - - out := bytes.NewBuffer(nil) - err = (&printer.Config{Mode: printer.TabIndent | printer.UseSpaces, Tabwidth: 8}).Fprint(out, fset, ast) - if err != nil { - gen.Fail("generated Go source code could not be reformatted:", err.Error()) - } - - return out.String() -} - -func (t *Base) PrintComments(comments typemap.DefinitionComments) bool { - text := strings.TrimSuffix(comments.Leading, "\n") - if len(strings.TrimSpace(text)) == 0 { - return false - } - split := strings.Split(text, "\n") - for _, line := range split { - t.P("// ", strings.TrimPrefix(line, " ")) - } - return len(split) > 0 -} - -// IsOwnPackage ... -// protoName is fully qualified name of a type -func (t *Base) IsOwnPackage(protoName string) bool { - def := t.Reg.MessageDefinition(protoName) - if def == nil { - gen.Fail("could not find message for", protoName) - } - return def.File.GetPackage() == t.PackageName -} - -// Given a protobuf name for a Message, return the Go name we will use for that -// type, including its package prefix. -func (t *Base) GoTypeName(protoName string) string { - def := t.Reg.MessageDefinition(protoName) - if def == nil { - gen.Fail("could not find message for", protoName) - } - - var prefix string - if def.File.GetPackage() != t.PackageName { - prefix = t.GoPackageNameForProtoFile(def.File) + "." - } - - var name string - for _, parent := range def.Lineage() { - name += parent.Descriptor.GetName() + "_" - } - name += def.Descriptor.GetName() - return prefix + name -} - -func streamingMethod(method *descriptor.MethodDescriptorProto) bool { - return (method.ServerStreaming != nil && *method.ServerStreaming) || (method.ClientStreaming != nil && *method.ClientStreaming) -} - -func (t *Base) ShouldGenForMethod(file *descriptor.FileDescriptorProto, - service *descriptor.ServiceDescriptorProto, - method *descriptor.MethodDescriptorProto) bool { - if streamingMethod(method) { - return false - } - if !t.Params.ExplicitHTTP { - return true - } - httpInfo := t.GetHttpInfoCached(file, service, method) - return httpInfo.HasExplicitHTTPPath -} -func (t *Base) SubstituteImportPath(importPath string, importFile string) string { - if substitution, ok := t.importMap[importFile]; ok { - importPath = substitution - } - return importPath -} diff --git a/tool/protobuf/pkg/generator/helper.go b/tool/protobuf/pkg/generator/helper.go deleted file mode 100644 index ae7e1fd62..000000000 --- a/tool/protobuf/pkg/generator/helper.go +++ /dev/null @@ -1,136 +0,0 @@ -package generator - -import ( - "reflect" - "strings" - - "github.com/golang/protobuf/proto" - "github.com/golang/protobuf/protoc-gen-go/descriptor" - - "github.com/go-kratos/kratos/tool/protobuf/pkg/extensions/gogoproto" - "github.com/go-kratos/kratos/tool/protobuf/pkg/tag" - "github.com/go-kratos/kratos/tool/protobuf/pkg/typemap" -) - -// GetJSONFieldName get name from gogoproto.jsontag -// else the original name -func GetJSONFieldName(field *descriptor.FieldDescriptorProto) string { - if field == nil { - return "" - } - if field.Options != nil { - v, err := proto.GetExtension(field.Options, gogoproto.E_Jsontag) - if err == nil && v.(*string) != nil { - ret := *(v.(*string)) - i := strings.Index(ret, ",") - if i != -1 { - ret = ret[:i] - } - return ret - } - } - return field.GetName() -} - -// GetFormOrJSONName get name from form tag, then json tag -// then original name -func GetFormOrJSONName(field *descriptor.FieldDescriptorProto) string { - if field == nil { - return "" - } - tags := tag.GetMoreTags(field) - if tags != nil { - tag := reflect.StructTag(*tags) - fName := tag.Get("form") - if fName != "" { - i := strings.Index(fName, ",") - if i != -1 { - fName = fName[:i] - } - return fName - } - } - return GetJSONFieldName(field) -} - -// IsScalar Is this field a scalar numeric type? -func IsScalar(field *descriptor.FieldDescriptorProto) bool { - if field.Type == nil { - return false - } - switch *field.Type { - case descriptor.FieldDescriptorProto_TYPE_DOUBLE, - descriptor.FieldDescriptorProto_TYPE_FLOAT, - descriptor.FieldDescriptorProto_TYPE_INT64, - descriptor.FieldDescriptorProto_TYPE_UINT64, - descriptor.FieldDescriptorProto_TYPE_INT32, - descriptor.FieldDescriptorProto_TYPE_FIXED64, - descriptor.FieldDescriptorProto_TYPE_FIXED32, - descriptor.FieldDescriptorProto_TYPE_BOOL, - descriptor.FieldDescriptorProto_TYPE_UINT32, - descriptor.FieldDescriptorProto_TYPE_ENUM, - descriptor.FieldDescriptorProto_TYPE_SFIXED32, - descriptor.FieldDescriptorProto_TYPE_SFIXED64, - descriptor.FieldDescriptorProto_TYPE_SINT32, - descriptor.FieldDescriptorProto_TYPE_SINT64, - descriptor.FieldDescriptorProto_TYPE_BYTES, - descriptor.FieldDescriptorProto_TYPE_STRING: - return true - default: - return false - } -} - -// IsMap is protocol buffer map -func IsMap(field *descriptor.FieldDescriptorProto, reg *typemap.Registry) bool { - if field.GetType() != descriptor.FieldDescriptorProto_TYPE_MESSAGE { - return false - } - md := reg.MessageDefinition(field.GetTypeName()) - if md == nil || !md.Descriptor.GetOptions().GetMapEntry() { - return false - } - return true -} - -// IsRepeated Is this field repeated? -func IsRepeated(field *descriptor.FieldDescriptorProto) bool { - return field.Label != nil && *field.Label == descriptor.FieldDescriptorProto_LABEL_REPEATED -} - -// GetFieldRequired is field required? -// eg. validate="required" -func GetFieldRequired( - f *descriptor.FieldDescriptorProto, - reg *typemap.Registry, - md *typemap.MessageDefinition, -) bool { - fComment, _ := reg.FieldComments(md, f) - var tags []reflect.StructTag - { - //get required info from gogoproto.moretags - moretags := tag.GetMoreTags(f) - if moretags != nil { - tags = []reflect.StructTag{reflect.StructTag(*moretags)} - } - } - if len(tags) == 0 { - tags = tag.GetTagsInComment(fComment.Leading) - } - validateTag := tag.GetTagValue("validate", tags) - var validateRules []string - if validateTag != "" { - validateRules = strings.Split(validateTag, ",") - } - required := false - for _, rule := range validateRules { - if rule == "required" { - required = true - } - } - return required -} - -func MakeIndentStr(i int) string { - return strings.Repeat(" ", i) -} diff --git a/tool/protobuf/pkg/generator/http.go b/tool/protobuf/pkg/generator/http.go deleted file mode 100644 index 0291752a8..000000000 --- a/tool/protobuf/pkg/generator/http.go +++ /dev/null @@ -1,146 +0,0 @@ -package generator - -import ( - "fmt" - "net/http" - "strings" - - "github.com/golang/protobuf/proto" - "github.com/golang/protobuf/protoc-gen-go/descriptor" - "google.golang.org/genproto/googleapis/api/annotations" - - "github.com/go-kratos/kratos/tool/protobuf/pkg/tag" - "github.com/go-kratos/kratos/tool/protobuf/pkg/typemap" -) - -// HTTPInfo http info for method -type HTTPInfo struct { - HttpMethod string - Path string - LegacyPath string - NewPath string - IsLegacyPath bool - Title string - Description string - // is http path added in the google.api.http option ? - HasExplicitHTTPPath bool -} - -type googleMethodOptionInfo struct { - Method string - PathPattern string - HTTPRule *annotations.HttpRule -} - -// GetHTTPInfo http info of method -func GetHTTPInfo( - file *descriptor.FileDescriptorProto, - service *descriptor.ServiceDescriptorProto, - method *descriptor.MethodDescriptorProto, - reg *typemap.Registry) *HTTPInfo { - var ( - title string - desc string - httpMethod string - newPath string - explicitHTTPPath bool - ) - comment, _ := reg.MethodComments(file, service, method) - tags := tag.GetTagsInComment(comment.Leading) - cleanComments := tag.GetCommentWithoutTag(comment.Leading) - if len(cleanComments) > 0 { - title = strings.Trim(cleanComments[0], "\n\r ") - if len(cleanComments) > 1 { - descLines := cleanComments[1:] - desc = strings.Trim(strings.Join(descLines, "\n"), "\r\n ") - } else { - desc = "" - } - } else { - title = "" - } - googleOptionInfo, err := ParseBMMethod(method) - if err == nil { - httpMethod = strings.ToUpper(googleOptionInfo.Method) - p := googleOptionInfo.PathPattern - if p != "" { - explicitHTTPPath = true - newPath = p - goto END - } - } - - if httpMethod == "" { - // resolve http method - httpMethod = tag.GetTagValue("method", tags) - if httpMethod == "" { - httpMethod = "GET" - } else { - httpMethod = strings.ToUpper(httpMethod) - } - } - - newPath = "/" + file.GetPackage() + "." + service.GetName() + "/" + method.GetName() -END: - var p = newPath - param := &HTTPInfo{HttpMethod: httpMethod, - Path: p, - NewPath: newPath, - IsLegacyPath: false, - Title: title, - Description: desc, - HasExplicitHTTPPath: explicitHTTPPath, - } - if title == "" { - param.Title = param.Path - } - return param -} - -func (t *Base) GetHttpInfoCached(file *descriptor.FileDescriptorProto, - service *descriptor.ServiceDescriptorProto, - method *descriptor.MethodDescriptorProto) *HTTPInfo { - key := file.GetPackage() + service.GetName() + method.GetName() - httpInfo, ok := t.httpInfoCache[key] - if !ok { - httpInfo = GetHTTPInfo(file, service, method, t.Reg) - t.httpInfoCache[key] = httpInfo - } - return httpInfo -} - -// ParseBMMethod parse BMMethodDescriptor form method descriptor proto -func ParseBMMethod(method *descriptor.MethodDescriptorProto) (*googleMethodOptionInfo, error) { - ext, err := proto.GetExtension(method.GetOptions(), annotations.E_Http) - if err != nil { - return nil, fmt.Errorf("get extension error: %s", err) - } - rule := ext.(*annotations.HttpRule) - var httpMethod string - var pathPattern string - switch pattern := rule.Pattern.(type) { - case *annotations.HttpRule_Get: - pathPattern = pattern.Get - httpMethod = http.MethodGet - case *annotations.HttpRule_Put: - pathPattern = pattern.Put - httpMethod = http.MethodPut - case *annotations.HttpRule_Post: - pathPattern = pattern.Post - httpMethod = http.MethodPost - case *annotations.HttpRule_Patch: - pathPattern = pattern.Patch - httpMethod = http.MethodPatch - case *annotations.HttpRule_Delete: - pathPattern = pattern.Delete - httpMethod = http.MethodDelete - default: - return nil, fmt.Errorf("unsupport http pattern %s", rule.Pattern) - } - bmMethod := &googleMethodOptionInfo{ - Method: httpMethod, - PathPattern: pathPattern, - HTTPRule: rule, - } - return bmMethod, nil -} diff --git a/tool/protobuf/pkg/naming/go_naming.go b/tool/protobuf/pkg/naming/go_naming.go deleted file mode 100644 index 3b428a6cf..000000000 --- a/tool/protobuf/pkg/naming/go_naming.go +++ /dev/null @@ -1,27 +0,0 @@ -package naming - -import ( - "path" - - "github.com/golang/protobuf/protoc-gen-go/descriptor" -) - -// GoFileName returns the output name for the generated Go file. -func GoFileName(f *descriptor.FileDescriptorProto, suffix string) string { - name := *f.Name - if ext := path.Ext(name); ext == ".pb" || ext == ".proto" || ext == ".protodevel" { - name = name[:len(name)-len(ext)] - } - name += suffix - - // Does the file have a "go_package" option? If it does, it may override the - // filename. - if impPath, _, ok := goPackageOption(f); ok && impPath != "" { - // Replace the existing dirname with the declared import path. - _, name = path.Split(name) - name = path.Join(impPath, name) - return name - } - - return name -} diff --git a/tool/protobuf/pkg/naming/naming.go b/tool/protobuf/pkg/naming/naming.go deleted file mode 100644 index bf18c5900..000000000 --- a/tool/protobuf/pkg/naming/naming.go +++ /dev/null @@ -1,113 +0,0 @@ -package naming - -import ( - "os" - "path" - "path/filepath" - "strings" - - "github.com/golang/protobuf/protoc-gen-go/descriptor" - "github.com/pkg/errors" - "github.com/siddontang/go/ioutil2" - - "github.com/go-kratos/kratos/tool/protobuf/pkg/utils" -) - -// GetVersionPrefix 根据go包名获取api版本前缀 -// @param pkg 从proto获取到的对应的go报名 -// @return 如果是v*开始的 返回v* -// 否则返回空 -func GetVersionPrefix(pkg string) string { - if pkg == "" { - return "" - } - if pkg[:1] == "v" { - return pkg - } - return "" -} - -// GenFileName returns the output name for the generated Go file. -func GenFileName(f *descriptor.FileDescriptorProto, suffix string) string { - name := *f.Name - if ext := path.Ext(name); ext == ".pb" || ext == ".proto" || ext == ".protodevel" { - name = name[:len(name)-len(ext)] - } - name += suffix - return name -} - -func ServiceName(service *descriptor.ServiceDescriptorProto) string { - return utils.CamelCase(service.GetName()) -} - -// MethodName ... -func MethodName(method *descriptor.MethodDescriptorProto) string { - return utils.CamelCase(method.GetName()) -} - -// GetGoImportPathForPb 得到 proto 文件对应的 go import路径 -// protoFilename is the proto file name -// 可能根本无法得到proto文件的具体路径, 只能假设 proto 的filename 是相对当前目录的 -// 假设 protoAbsolutePath = wd/protoFilename -func GetGoImportPathForPb(protoFilename string, moduleImportPath string, moduleDirName string) (importPath string, err error) { - wd, err := os.Getwd() - if err != nil { - panic("cannot get working directory") - } - absPath := wd + "/" + protoFilename - if !ioutil2.FileExists(absPath) { - err = errors.New("Cannot find proto file path of " + protoFilename) - return "", err - } - index := strings.Index(absPath, moduleDirName) - if index == -1 { - return "", errors.Errorf("proto file %s is not inside project %s", protoFilename, moduleDirName) - } - relativePath := absPath[index:] - importPath = filepath.Dir(relativePath) - return importPath, nil -} - -// GoPackageNameForProtoFile returns the Go package name to use in the generated Go file. -// The result explicitly reports whether the name came from an option go_package -// statement. If explicit is false, the name was derived from the protocol -// buffer's package statement or the input file name. -func GoPackageName(f *descriptor.FileDescriptorProto) (name string, explicit bool) { - // Does the file have a "go_package" option? - if _, pkg, ok := goPackageOption(f); ok { - return pkg, true - } - - // Does the file have a package clause? - if pkg := f.GetPackage(); pkg != "" { - return pkg, false - } - // Use the file base name. - return utils.BaseName(f.GetName()), false -} - -// goPackageOption interprets the file's go_package option. -// If there is no go_package, it returns ("", "", false). -// If there's a simple name, it returns ("", pkg, true). -// If the option implies an import path, it returns (impPath, pkg, true). -func goPackageOption(f *descriptor.FileDescriptorProto) (impPath, pkg string, ok bool) { - pkg = f.GetOptions().GetGoPackage() - if pkg == "" { - return - } - ok = true - // The presence of a slash implies there's an import path. - slash := strings.LastIndex(pkg, "/") - if slash < 0 { - return - } - impPath, pkg = pkg, pkg[slash+1:] - // A semicolon-delimited suffix overrides the package name. - sc := strings.IndexByte(impPath, ';') - if sc < 0 { - return - } - impPath, pkg = impPath[:sc], impPath[sc+1:] - return -} diff --git a/tool/protobuf/pkg/project/project.go b/tool/protobuf/pkg/project/project.go deleted file mode 100644 index 537909ff5..000000000 --- a/tool/protobuf/pkg/project/project.go +++ /dev/null @@ -1,121 +0,0 @@ -package project - -import ( - "os" - "path/filepath" - "strings" - - "github.com/pkg/errors" - "github.com/siddontang/go/ioutil2" - - "github.com/go-kratos/kratos/tool/protobuf/pkg/utils" -) - -// if proto file is inside a project (that has a /api directory) -// this present a project info -// 必须假设proto文件的路径就是相对work-dir的路径,否则无法找到proto文件以及对应的project -type ProjectInfo struct { - // AbsolutePath of the project - AbsolutePath string - // ImportPath of project - ImportPath string - // dir name of project - Name string - // parent dir of project, maybe empty - Department string - // grandma dir of project , maybe empty - Typ string - HasInternalPkg bool - // 从工作目录(working directory)到project目录的相对路径 比如a/b .. ../a - // 作用是什么? - // 假设目录结构是 - // -project - // - api/api.proto - // - internal/service - // 我想在 internal/service下生成一个文件 service.go - // work-dir 为 project/api - // proto 生成命令为 protoc --xx_out=. api.proto - // 那么在 protoc plugin 中的文件输出路径就得是 ../internal/service/ => {pathRefToProj}internal/service - // - PathRefToProj string -} - -func NewProjInfo(file string, modDirName string, modImportPath string) (projInfo *ProjectInfo, err error) { - projInfo = &ProjectInfo{} - wd, err := os.Getwd() - if err != nil { - panic("cannot get working directory") - } - protoAbs := wd + "/" + file - protoAbs, _ = filepath.Abs(protoAbs) - - if !ioutil2.FileExists(protoAbs) { - return nil, errors.Errorf("Cannot find proto file in current dir %s, file: %s ", wd, file) - } - //appIndex := strings.Index(wd, modDirName) - //if appIndex == -1 { - // err = errors.New("not in " + modDirName) - // return nil, err - //} - - projPath := LookupProjPath(protoAbs) - - if projPath == "" { - err = errors.New("not in project") - return nil, err - } - rel, _ := filepath.Rel(wd, projPath) - projInfo.PathRefToProj = rel - projInfo.AbsolutePath = projPath - if ioutil2.FileExists(projPath + "/internal") { - projInfo.HasInternalPkg = true - } - - i := strings.Index(projInfo.AbsolutePath, modDirName) - if i == -1 { - err = errors.Errorf("project is not inside module, project=%s, module=%s", projPath, modDirName) - return nil, err - } - relativePath := projInfo.AbsolutePath[i+len(modDirName):] - projInfo.ImportPath = modImportPath + relativePath - projInfo.Name = filepath.Base(projPath) - if p := filepath.Dir(projPath); p != "/" { - projInfo.Department = filepath.Base(p) - if p = filepath.Dir(p); p != "/" { - projInfo.Typ = filepath.Base(p) - } - } - return projInfo, nil -} - -// LookupProjPath get project path by proto absolute path -// assume that proto is in the project's api directory -func LookupProjPath(protoAbs string) (result string) { - f := func(protoAbs string, dirs []string) string { - lastIndex := len(protoAbs) - curPath := protoAbs - - for lastIndex > 0 { - found := true - for _, d := range dirs { - if !utils.IsDir(curPath + "/" + d) { - found = false - break - } - } - if found { - return curPath - } - lastIndex = strings.LastIndex(curPath, string(os.PathSeparator)) - curPath = protoAbs[:lastIndex] - } - result = "" - return result - } - - firstStep := f(protoAbs, []string{"cmd", "api"}) - if firstStep != "" { - return firstStep - } - return f(protoAbs, []string{"api"}) -} diff --git a/tool/protobuf/pkg/tag/ext_tags.go b/tool/protobuf/pkg/tag/ext_tags.go deleted file mode 100644 index d2bc6726a..000000000 --- a/tool/protobuf/pkg/tag/ext_tags.go +++ /dev/null @@ -1,21 +0,0 @@ -package tag - -import ( - "github.com/golang/protobuf/proto" - "github.com/golang/protobuf/protoc-gen-go/descriptor" - - "github.com/go-kratos/kratos/tool/protobuf/pkg/extensions/gogoproto" -) - -func GetMoreTags(field *descriptor.FieldDescriptorProto) *string { - if field == nil { - return nil - } - if field.Options != nil { - v, err := proto.GetExtension(field.Options, gogoproto.E_Moretags) - if err == nil && v.(*string) != nil { - return v.(*string) - } - } - return nil -} diff --git a/tool/protobuf/pkg/tag/tags.go b/tool/protobuf/pkg/tag/tags.go deleted file mode 100644 index e9f0e9c85..000000000 --- a/tool/protobuf/pkg/tag/tags.go +++ /dev/null @@ -1,55 +0,0 @@ -package tag - -import ( - "reflect" - "strings" -) - -// GetCommentWithoutTag strip tags in comment -func GetCommentWithoutTag(comment string) []string { - var lines []string - if comment == "" { - return lines - } - split := strings.Split(strings.TrimRight(comment, "\n\r"), "\n") - for _, line := range split { - tag, _, _ := GetLineTag(line) - if tag == "" { - lines = append(lines, line) - } - } - return lines -} - -func GetTagsInComment(comment string) []reflect.StructTag { - split := strings.Split(comment, "\n") - var tagsInComment []reflect.StructTag - for _, line := range split { - tag, _, _ := GetLineTag(line) - if tag != "" { - tagsInComment = append(tagsInComment, tag) - } - } - return tagsInComment -} - -func GetTagValue(key string, tags []reflect.StructTag) string { - for _, t := range tags { - val := t.Get(key) - if val != "" { - return val - } - } - return "" -} - -// find tag between backtick, start & end is the position of backtick -func GetLineTag(line string) (tag reflect.StructTag, start int, end int) { - start = strings.Index(line, "`") - end = strings.LastIndex(line, "`") - if end <= start { - return - } - tag = reflect.StructTag(line[start+1 : end]) - return -} diff --git a/tool/protobuf/pkg/typemap/typemap.go b/tool/protobuf/pkg/typemap/typemap.go deleted file mode 100644 index 27344750c..000000000 --- a/tool/protobuf/pkg/typemap/typemap.go +++ /dev/null @@ -1,277 +0,0 @@ -package typemap - -// Copyright 2018 Twitch Interactive, Inc. All Rights Reserved. -// -// Licensed under the Apache License, Version 2.0 (the "License"). You may not -// use this file except in compliance with the License. A copy of the License is -// located at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// or in the "license" file accompanying this file. This file is distributed on -// an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either -// express or implied. See the License for the specific language governing -// permissions and limitations under the License. - -import ( - "strings" - - "github.com/golang/protobuf/protoc-gen-go/descriptor" - "github.com/pkg/errors" -) - -// Registry is the place of descriptors resolving -type Registry struct { - allFiles []*descriptor.FileDescriptorProto - filesByName map[string]*descriptor.FileDescriptorProto - - // Mapping of fully-qualified names to their definitions - messagesByProtoName map[string]*MessageDefinition -} - -// New Registry -func New(files []*descriptor.FileDescriptorProto) *Registry { - r := &Registry{ - allFiles: files, - filesByName: make(map[string]*descriptor.FileDescriptorProto), - messagesByProtoName: make(map[string]*MessageDefinition), - } - - // First, index the file descriptors by name. We need this so - // messageDefsForFile can correctly scan imports. - for _, f := range files { - r.filesByName[f.GetName()] = f - } - - // Next, index all the message definitions by their fully-qualified proto - // names. - for _, f := range files { - defs := messageDefsForFile(f, r.filesByName) - for name, def := range defs { - r.messagesByProtoName[name] = def - } - } - return r -} - -// FileComments comment of file -func (r *Registry) FileComments(file *descriptor.FileDescriptorProto) (DefinitionComments, error) { - return commentsAtPath([]int32{packagePath}, file), nil -} - -// ServiceComments comments of service -func (r *Registry) ServiceComments(file *descriptor.FileDescriptorProto, svc *descriptor.ServiceDescriptorProto) (DefinitionComments, error) { - for i, s := range file.Service { - if s == svc { - path := []int32{servicePath, int32(i)} - return commentsAtPath(path, file), nil - } - } - return DefinitionComments{}, errors.Errorf("service not found in file") -} - -// FieldComments ... -func (r *Registry) FieldComments(message *MessageDefinition, field *descriptor.FieldDescriptorProto) (DefinitionComments, error) { - file := message.File - mpath := message.path - for i, f := range message.Descriptor.Field { - if f == field { - path := append(mpath, messageFieldPath, int32(i)) - return commentsAtPath(path, file), nil - } - } - return DefinitionComments{}, errors.Errorf("field not found in msg") -} - -// MethodComments comment of method -func (r *Registry) MethodComments(file *descriptor.FileDescriptorProto, svc *descriptor.ServiceDescriptorProto, method *descriptor.MethodDescriptorProto) (DefinitionComments, error) { - for i, s := range file.Service { - if s == svc { - path := []int32{servicePath, int32(i)} - for j, m := range s.Method { - if m == method { - path = append(path, serviceMethodPath, int32(j)) - return commentsAtPath(path, file), nil - } - } - } - } - return DefinitionComments{}, errors.Errorf("service not found in file") -} - -// MethodInputDefinition returns MethodInputDefinition -func (r *Registry) MethodInputDefinition(method *descriptor.MethodDescriptorProto) *MessageDefinition { - return r.messagesByProtoName[method.GetInputType()] -} - -// MethodOutputDefinition returns MethodOutputDefinition -func (r *Registry) MethodOutputDefinition(method *descriptor.MethodDescriptorProto) *MessageDefinition { - return r.messagesByProtoName[method.GetOutputType()] -} - -// MessageDefinition by name -func (r *Registry) MessageDefinition(name string) *MessageDefinition { - return r.messagesByProtoName[name] -} - -// MessageDefinition msg info -type MessageDefinition struct { - // Descriptor is is the DescriptorProto defining the message. - Descriptor *descriptor.DescriptorProto - // File is the File that the message was defined in. Or, if it has been - // publicly imported, what File was that import performed in? - File *descriptor.FileDescriptorProto - // Parent is the parent message, if this was defined as a nested message. If - // this was defiend at the top level, parent is nil. - Parent *MessageDefinition - // Comments describes the comments surrounding a message's definition. If it - // was publicly imported, then these comments are from the actual source file, - // not the file that the import was performed in. - Comments DefinitionComments - - // path is the 'SourceCodeInfo' path. See the documentation for - // github.com/golang/protobuf/protoc-gen-go/descriptor.SourceCodeInfo for an - // explanation of its format. - path []int32 -} - -// ProtoName returns the dot-delimited, fully-qualified protobuf name of the -// message. -func (m *MessageDefinition) ProtoName() string { - prefix := "." - if pkg := m.File.GetPackage(); pkg != "" { - prefix += pkg + "." - } - - if lineage := m.Lineage(); len(lineage) > 0 { - for _, parent := range lineage { - prefix += parent.Descriptor.GetName() + "." - } - } - - return prefix + m.Descriptor.GetName() -} - -// Lineage returns m's parental chain all the way back up to a top-level message -// definition. The first element of the returned slice is the highest-level -// parent. -func (m *MessageDefinition) Lineage() []*MessageDefinition { - var parents []*MessageDefinition - for p := m.Parent; p != nil; p = p.Parent { - parents = append([]*MessageDefinition{p}, parents...) - } - return parents -} - -// descendants returns all the submessages defined within m, and all the -// descendants of those, recursively. -func (m *MessageDefinition) descendants() []*MessageDefinition { - descendants := make([]*MessageDefinition, 0) - for i, child := range m.Descriptor.NestedType { - path := append(m.path, []int32{messageMessagePath, int32(i)}...) - childDef := &MessageDefinition{ - Descriptor: child, - File: m.File, - Parent: m, - Comments: commentsAtPath(path, m.File), - path: path, - } - descendants = append(descendants, childDef) - descendants = append(descendants, childDef.descendants()...) - } - return descendants -} - -// messageDefsForFile gathers a mapping of fully-qualified protobuf names to -// their definitions. It scans a singles file at a time. It requires a mapping -// of .proto file names to their definitions in order to correctly handle -// 'import public' declarations; this mapping should include all files -// transitively imported by f. -func messageDefsForFile(f *descriptor.FileDescriptorProto, filesByName map[string]*descriptor.FileDescriptorProto) map[string]*MessageDefinition { - byProtoName := make(map[string]*MessageDefinition) - // First, gather all the messages defined at the top level. - for i, d := range f.MessageType { - path := []int32{messagePath, int32(i)} - def := &MessageDefinition{ - Descriptor: d, - File: f, - Parent: nil, - Comments: commentsAtPath(path, f), - path: path, - } - - byProtoName[def.ProtoName()] = def - // Next, all nested message definitions. - for _, child := range def.descendants() { - byProtoName[child.ProtoName()] = child - } - } - - // Finally, all messages imported publicly. - for _, depIdx := range f.PublicDependency { - depFileName := f.Dependency[depIdx] - depFile := filesByName[depFileName] - depDefs := messageDefsForFile(depFile, filesByName) - for _, def := range depDefs { - imported := &MessageDefinition{ - Descriptor: def.Descriptor, - File: f, - Parent: def.Parent, - Comments: commentsAtPath(def.path, depFile), - path: def.path, - } - byProtoName[imported.ProtoName()] = imported - } - } - - return byProtoName -} - -// // ignored detached comments. -type DefinitionComments struct { - Leading string - Trailing string - LeadingDetached []string -} - -func commentsAtPath(path []int32, sourceFile *descriptor.FileDescriptorProto) DefinitionComments { - if sourceFile.SourceCodeInfo == nil { - // The compiler didn't provide us with comments. - return DefinitionComments{} - } - - for _, loc := range sourceFile.SourceCodeInfo.Location { - if pathEqual(path, loc.Path) { - return DefinitionComments{ - Leading: strings.TrimSuffix(loc.GetLeadingComments(), "\n"), - LeadingDetached: loc.GetLeadingDetachedComments(), - Trailing: loc.GetTrailingComments(), - } - } - } - return DefinitionComments{} -} - -func pathEqual(path1, path2 []int32) bool { - if len(path1) != len(path2) { - return false - } - for i, v := range path1 { - if path2[i] != v { - return false - } - } - return true -} - -const ( - // tag numbers in FileDescriptorProto - packagePath = 2 // package - messagePath = 4 // message_type - servicePath = 6 // service - // tag numbers in DescriptorProto - messageFieldPath = 2 // field - messageMessagePath = 3 // nested_type - // tag numbers in ServiceDescriptorProto - serviceMethodPath = 2 // method -) diff --git a/tool/protobuf/pkg/utils/stringutils.go b/tool/protobuf/pkg/utils/stringutils.go deleted file mode 100644 index 8da21c8fe..000000000 --- a/tool/protobuf/pkg/utils/stringutils.go +++ /dev/null @@ -1,97 +0,0 @@ -package utils - -import ( - "strings" - "unicode" -) - -// Is c an ASCII lower-case letter? -func isASCIILower(c byte) bool { - return 'a' <= c && c <= 'z' -} - -// Is c an ASCII digit? -func isASCIIDigit(c byte) bool { - return '0' <= c && c <= '9' -} - -// CamelCase converts a string from snake_case to CamelCased. -// -// If there is an interior underscore followed by a lower case letter, drop the -// underscore and convert the letter to upper case. There is a remote -// possibility of this rewrite causing a name collision, but it's so remote -// we're prepared to pretend it's nonexistent - since the C++ generator -// lowercases names, it's extremely unlikely to have two fields with different -// capitalizations. In short, _my_field_name_2 becomes XMyFieldName_2. -func CamelCase(s string) string { - if s == "" { - return "" - } - t := make([]byte, 0, 32) - i := 0 - if s[0] == '_' { - // Need a capital letter; drop the '_'. - t = append(t, 'X') - i++ - } - // Invariant: if the next letter is lower case, it must be converted - // to upper case. - // - // That is, we process a word at a time, where words are marked by _ or upper - // case letter. Digits are treated as words. - for ; i < len(s); i++ { - c := s[i] - if c == '_' && i+1 < len(s) && isASCIILower(s[i+1]) { - continue // Skip the underscore in s. - } - if isASCIIDigit(c) { - t = append(t, c) - continue - } - // Assume we have a letter now - if not, it's a bogus identifier. The next - // word is a sequence of characters that must start upper case. - if isASCIILower(c) { - c ^= ' ' // Make it a capital letter. - } - t = append(t, c) // Guaranteed not lower case. - // Accept lower case sequence that follows. - for i+1 < len(s) && isASCIILower(s[i+1]) { - i++ - t = append(t, s[i]) - } - } - return string(t) -} - -// CamelCaseSlice is like CamelCase, but the argument is a slice of strings to -// be joined with "_" and then camelcased. -func CamelCaseSlice(elem []string) string { return CamelCase(strings.Join(elem, "_")) } - -// BaseName the last path element of a slash-delimited name, with the last -// dotted suffix removed. -func BaseName(name string) string { - // First, find the last element - if i := strings.LastIndex(name, "/"); i >= 0 { - name = name[i+1:] - } - // Now drop the suffix - if i := strings.LastIndex(name, "."); i >= 0 { - name = name[0:i] - } - return name -} - -// AlphaDigitize replaces non-letter, non-digit, non-underscore characters with -// underscore. -func AlphaDigitize(r rune) rune { - if unicode.IsLetter(r) || unicode.IsDigit(r) || r == '_' { - return r - } - return '_' -} - -// CleanIdentifier makes sure s is a valid 'identifier' string: it contains only -// letters, numbers, and underscore. -func CleanIdentifier(s string) string { - return strings.Map(AlphaDigitize, s) -} diff --git a/tool/protobuf/pkg/utils/utils.go b/tool/protobuf/pkg/utils/utils.go deleted file mode 100644 index dcf0c3f14..000000000 --- a/tool/protobuf/pkg/utils/utils.go +++ /dev/null @@ -1,29 +0,0 @@ -package utils - -import ( - "os" - "unicode" -) - -// LcFirst lower the first letter -func LcFirst(str string) string { - for i, v := range str { - return string(unicode.ToLower(v)) + str[i+1:] - } - return "" -} - -func IsDir(name string) bool { - file, err := os.Open(name) - - if err != nil { - return false - } - defer file.Close() - - fi, err := file.Stat() - if err != nil { - return false - } - return fi.IsDir() -} diff --git a/tool/protobuf/protoc-gen-bm/generator/generator.go b/tool/protobuf/protoc-gen-bm/generator/generator.go deleted file mode 100644 index 31becf28e..000000000 --- a/tool/protobuf/protoc-gen-bm/generator/generator.go +++ /dev/null @@ -1,337 +0,0 @@ -package generator - -import ( - "fmt" - "reflect" - "sort" - "strings" - - "github.com/golang/protobuf/proto" - "github.com/golang/protobuf/protoc-gen-go/descriptor" - plugin "github.com/golang/protobuf/protoc-gen-go/plugin" - - "github.com/go-kratos/kratos/tool/protobuf/pkg/generator" - "github.com/go-kratos/kratos/tool/protobuf/pkg/naming" - "github.com/go-kratos/kratos/tool/protobuf/pkg/tag" - "github.com/go-kratos/kratos/tool/protobuf/pkg/typemap" - "github.com/go-kratos/kratos/tool/protobuf/pkg/utils" -) - -type bm struct { - generator.Base - filesHandled int -} - -// BmGenerator BM generator. -func BmGenerator() *bm { - t := &bm{} - return t -} - -// Generate ... -func (t *bm) Generate(in *plugin.CodeGeneratorRequest) *plugin.CodeGeneratorResponse { - t.Setup(in) - - // Showtime! Generate the response. - resp := new(plugin.CodeGeneratorResponse) - for _, f := range t.GenFiles { - respFile := t.generateForFile(f) - if respFile != nil { - resp.File = append(resp.File, respFile) - } - } - return resp -} - -func (t *bm) generateForFile(file *descriptor.FileDescriptorProto) *plugin.CodeGeneratorResponse_File { - resp := new(plugin.CodeGeneratorResponse_File) - - t.generateFileHeader(file, t.GenPkgName) - t.generateImports(file) - t.generatePathConstants(file) - count := 0 - for i, service := range file.Service { - count += t.generateBMInterface(file, service) - t.generateBMRoute(file, service, i) - } - - resp.Name = proto.String(naming.GenFileName(file, ".bm.go")) - resp.Content = proto.String(t.FormattedOutput()) - t.Output.Reset() - - t.filesHandled++ - return resp -} - -func (t *bm) generatePathConstants(file *descriptor.FileDescriptorProto) { - t.P() - for _, service := range file.Service { - name := naming.ServiceName(service) - for _, method := range service.Method { - if !t.ShouldGenForMethod(file, service, method) { - continue - } - apiInfo := t.GetHttpInfoCached(file, service, method) - t.P(`var Path`, name, naming.MethodName(method), ` = "`, apiInfo.Path, `"`) - } - t.P() - } -} - -func (t *bm) generateFileHeader(file *descriptor.FileDescriptorProto, pkgName string) { - t.P("// Code generated by protoc-gen-bm ", generator.Version, ", DO NOT EDIT.") - t.P("// source: ", file.GetName()) - t.P() - if t.filesHandled == 0 { - comment, err := t.Reg.FileComments(file) - if err == nil && comment.Leading != "" { - // doc for the first file - t.P("/*") - t.P("Package ", t.GenPkgName, " is a generated blademaster stub package.") - t.P("This code was generated with kratos/tool/protobuf/protoc-gen-bm ", generator.Version, ".") - t.P() - for _, line := range strings.Split(comment.Leading, "\n") { - line = strings.TrimPrefix(line, " ") - // ensure we don't escape from the block comment - line = strings.Replace(line, "*/", "* /", -1) - t.P(line) - } - t.P() - t.P("It is generated from these files:") - for _, f := range t.GenFiles { - t.P("\t", f.GetName()) - } - t.P("*/") - } - } - t.P(`package `, pkgName) - t.P() -} - -func (t *bm) generateImports(file *descriptor.FileDescriptorProto) { - //if len(file.Service) == 0 { - // return - //} - t.P(`import (`) - //t.P(` `,t.pkgs["context"], ` "context"`) - t.P(` "context"`) - t.P() - t.P(` bm "github.com/go-kratos/kratos/pkg/net/http/blademaster"`) - t.P(` "github.com/go-kratos/kratos/pkg/net/http/blademaster/binding"`) - - t.P(`)`) - // It's legal to import a message and use it as an input or output for a - // method. Make sure to import the package of any such message. First, dedupe - // them. - deps := t.DeduceDeps(file) - for pkg, importPath := range deps { - t.P(`import `, pkg, ` `, importPath) - } - t.P() - t.P(`// to suppressed 'imported but not used warning'`) - t.P(`var _ *bm.Context`) - t.P(`var _ context.Context`) - t.P(`var _ binding.StructValidator`) -} - -// Big header comments to makes it easier to visually parse a generated file. -func (t *bm) sectionComment(sectionTitle string) { - t.P() - t.P(`// `, strings.Repeat("=", len(sectionTitle))) - t.P(`// `, sectionTitle) - t.P(`// `, strings.Repeat("=", len(sectionTitle))) - t.P() -} - -func (t *bm) generateBMRoute( - file *descriptor.FileDescriptorProto, - service *descriptor.ServiceDescriptorProto, - index int) { - // old mode is generate xx.route.go in the http pkg - // new mode is generate route code in the same .bm.go - // route rule /x{department}/{project-name}/{path_prefix}/method_name - // generate each route method - servName := naming.ServiceName(service) - versionPrefix := naming.GetVersionPrefix(t.GenPkgName) - svcName := utils.LcFirst(utils.CamelCase(versionPrefix)) + servName + "Svc" - t.P(`var `, svcName, ` `, servName, `BMServer`) - - type methodInfo struct { - midwares []string - routeFuncName string - apiInfo *generator.HTTPInfo - methodName string - } - var methList []methodInfo - var allMidwareMap = make(map[string]bool) - var isLegacyPkg = false - for _, method := range service.Method { - if !t.ShouldGenForMethod(file, service, method) { - continue - } - var midwares []string - comments, _ := t.Reg.MethodComments(file, service, method) - tags := tag.GetTagsInComment(comments.Leading) - if tag.GetTagValue("dynamic", tags) == "true" { - continue - } - apiInfo := t.GetHttpInfoCached(file, service, method) - isLegacyPkg = apiInfo.IsLegacyPath - //httpMethod, legacyPath, path := getHttpInfo(file, service, method, t.reg) - //if legacyPath != "" { - // isLegacyPkg = true - //} - - midStr := tag.GetTagValue("midware", tags) - if midStr != "" { - midwares = strings.Split(midStr, ",") - for _, m := range midwares { - allMidwareMap[m] = true - } - } - - methName := naming.MethodName(method) - inputType := t.GoTypeName(method.GetInputType()) - - routeName := utils.LcFirst(utils.CamelCase(servName) + - utils.CamelCase(methName)) - - methList = append(methList, methodInfo{ - apiInfo: apiInfo, - midwares: midwares, - routeFuncName: routeName, - methodName: method.GetName(), - }) - - t.P(fmt.Sprintf("func %s (c *bm.Context) {", routeName)) - t.P(` p := new(`, inputType, `)`) - requestBinding := "" - if t.hasHeaderTag(t.Reg.MessageDefinition(method.GetInputType())) { - requestBinding = ", binding.Request" - } - t.P(` if err := c.BindWith(p, binding.Default(c.Request.Method, c.Request.Header.Get("Content-Type"))` + - requestBinding + `); err != nil {`) - t.P(` return`) - t.P(` }`) - t.P(` resp, err := `, svcName, `.`, methName, `(c, p)`) - t.P(` c.JSON(resp, err)`) - t.P(`}`) - t.P(``) - } - - // generate route group - var midList []string - for m := range allMidwareMap { - midList = append(midList, m+" bm.HandlerFunc") - } - - sort.Strings(midList) - - // 注册老的路由的方法 - if isLegacyPkg { - funcName := `Register` + utils.CamelCase(versionPrefix) + servName + `Service` - t.P(`// `, funcName, ` Register the blademaster route with middleware map`) - t.P(`// midMap is the middleware map, the key is defined in proto`) - t.P(`func `, funcName, `(e *bm.Engine, svc `, servName, "BMServer, midMap map[string]bm.HandlerFunc)", ` {`) - var keys []string - for m := range allMidwareMap { - keys = append(keys, m) - } - // to keep generated code consistent - sort.Strings(keys) - for _, m := range keys { - t.P(m, ` := midMap["`, m, `"]`) - } - - t.P(svcName, ` = svc`) - for _, methInfo := range methList { - var midArgStr string - if len(methInfo.midwares) == 0 { - midArgStr = "" - } else { - midArgStr = strings.Join(methInfo.midwares, ", ") + ", " - } - t.P(`e.`, methInfo.apiInfo.HttpMethod, `("`, methInfo.apiInfo.LegacyPath, `", `, midArgStr, methInfo.routeFuncName, `)`) - } - t.P(` }`) - } else { - // 新的注册路由的方法 - var bmFuncName = fmt.Sprintf("Register%sBMServer", servName) - t.P(`// `, bmFuncName, ` Register the blademaster route`) - t.P(`func `, bmFuncName, `(e *bm.Engine, server `, servName, `BMServer) {`) - t.P(svcName, ` = server`) - for _, methInfo := range methList { - t.P(`e.`, methInfo.apiInfo.HttpMethod, `("`, methInfo.apiInfo.NewPath, `",`, methInfo.routeFuncName, ` )`) - } - t.P(` }`) - } -} - -func (t *bm) hasHeaderTag(md *typemap.MessageDefinition) bool { - if md.Descriptor.Field == nil { - return false - } - for _, f := range md.Descriptor.Field { - t := tag.GetMoreTags(f) - if t != nil { - st := reflect.StructTag(*t) - if st.Get("request") != "" { - return true - } - if st.Get("header") != "" { - return true - } - } - } - return false -} - -func (t *bm) generateBMInterface(file *descriptor.FileDescriptorProto, service *descriptor.ServiceDescriptorProto) int { - count := 0 - servName := naming.ServiceName(service) - t.P("// " + servName + "BMServer is the server API for " + servName + " service.") - - comments, err := t.Reg.ServiceComments(file, service) - if err == nil { - t.PrintComments(comments) - } - t.P(`type `, servName, `BMServer interface {`) - for _, method := range service.Method { - if !t.ShouldGenForMethod(file, service, method) { - continue - } - count++ - t.generateInterfaceMethod(file, service, method, comments) - t.P() - } - t.P(`}`) - return count -} - -func (t *bm) generateInterfaceMethod(file *descriptor.FileDescriptorProto, - service *descriptor.ServiceDescriptorProto, - method *descriptor.MethodDescriptorProto, - comments typemap.DefinitionComments) { - comments, err := t.Reg.MethodComments(file, service, method) - - methName := naming.MethodName(method) - outputType := t.GoTypeName(method.GetOutputType()) - inputType := t.GoTypeName(method.GetInputType()) - tags := tag.GetTagsInComment(comments.Leading) - if tag.GetTagValue("dynamic", tags) == "true" { - return - } - - if err == nil { - t.PrintComments(comments) - } - - respDynamic := tag.GetTagValue("dynamic_resp", tags) == "true" - if respDynamic { - t.P(fmt.Sprintf(` %s(ctx context.Context, req *%s) (resp interface{}, err error)`, - methName, inputType)) - } else { - t.P(fmt.Sprintf(` %s(ctx context.Context, req *%s) (resp *%s, err error)`, - methName, inputType, outputType)) - } -} diff --git a/tool/protobuf/protoc-gen-bm/generator/generator_test.go b/tool/protobuf/protoc-gen-bm/generator/generator_test.go deleted file mode 100644 index 7a5d0bc68..000000000 --- a/tool/protobuf/protoc-gen-bm/generator/generator_test.go +++ /dev/null @@ -1,27 +0,0 @@ -package generator - -import ( - "os" - "os/exec" - "testing" - - "github.com/golang/protobuf/proto" - plugin "github.com/golang/protobuf/protoc-gen-go/plugin" -) - -func TestGenerateParseCommandLineParamsError(t *testing.T) { - if os.Getenv("BE_CRASHER") == "1" { - g := &bm{} - g.Generate(&plugin.CodeGeneratorRequest{ - Parameter: proto.String("invalid"), - }) - return - } - cmd := exec.Command(os.Args[0], "-test.run=TestGenerateParseCommandLineParamsError") - cmd.Env = append(os.Environ(), "BE_CRASHER=1") - err := cmd.Run() - if e, ok := err.(*exec.ExitError); ok && !e.Success() { - return - } - t.Fatalf("process ran with err %v, want exit status 1", err) -} diff --git a/tool/protobuf/protoc-gen-bm/main.go b/tool/protobuf/protoc-gen-bm/main.go deleted file mode 100644 index 95e8c3927..000000000 --- a/tool/protobuf/protoc-gen-bm/main.go +++ /dev/null @@ -1,23 +0,0 @@ -package main - -import ( - "flag" - "fmt" - "os" - - "github.com/go-kratos/kratos/tool/protobuf/pkg/gen" - "github.com/go-kratos/kratos/tool/protobuf/pkg/generator" - bmgen "github.com/go-kratos/kratos/tool/protobuf/protoc-gen-bm/generator" -) - -func main() { - versionFlag := flag.Bool("version", false, "print version and exit") - flag.Parse() - if *versionFlag { - fmt.Println(generator.Version) - os.Exit(0) - } - - g := bmgen.BmGenerator() - gen.Main(g) -} diff --git a/tool/protobuf/protoc-gen-bswagger/generator.go b/tool/protobuf/protoc-gen-bswagger/generator.go deleted file mode 100644 index 342522bcd..000000000 --- a/tool/protobuf/protoc-gen-bswagger/generator.go +++ /dev/null @@ -1,342 +0,0 @@ -package main - -import ( - "encoding/json" - "net/http" - "reflect" - "regexp" - "strings" - - "github.com/golang/protobuf/protoc-gen-go/descriptor" - plugin "github.com/golang/protobuf/protoc-gen-go/plugin" - - "github.com/go-kratos/kratos/tool/protobuf/pkg/gen" - "github.com/go-kratos/kratos/tool/protobuf/pkg/generator" - "github.com/go-kratos/kratos/tool/protobuf/pkg/naming" - "github.com/go-kratos/kratos/tool/protobuf/pkg/tag" - "github.com/go-kratos/kratos/tool/protobuf/pkg/typemap" -) - -type swaggerGen struct { - generator.Base - // defsMap will fill into swagger's definitions - // key is full qualified proto name - defsMap map[string]*typemap.MessageDefinition -} - -// NewSwaggerGenerator a swagger generator -func NewSwaggerGenerator() *swaggerGen { - return &swaggerGen{} -} - -func (t *swaggerGen) Generate(in *plugin.CodeGeneratorRequest) *plugin.CodeGeneratorResponse { - t.Setup(in) - resp := &plugin.CodeGeneratorResponse{} - for _, f := range t.GenFiles { - if len(f.Service) == 0 { - continue - } - respFile := t.generateSwagger(f) - if respFile != nil { - resp.File = append(resp.File, respFile) - } - } - return resp -} - -func (t *swaggerGen) generateSwagger(file *descriptor.FileDescriptorProto) *plugin.CodeGeneratorResponse_File { - var pkg = file.GetPackage() - r := regexp.MustCompile("v(\\d+)$") - strs := r.FindStringSubmatch(pkg) - var vStr string - if len(strs) >= 2 { - vStr = strs[1] - } else { - vStr = "" - } - var swaggerObj = &swaggerObject{ - Paths: swaggerPathsObject{}, - Swagger: "2.0", - Info: swaggerInfoObject{ - Title: file.GetName(), - Version: vStr, - }, - Schemes: []string{"http", "https"}, - Consumes: []string{"application/json", "multipart/form-data"}, - Produces: []string{"application/json"}, - } - t.defsMap = map[string]*typemap.MessageDefinition{} - - out := &plugin.CodeGeneratorResponse_File{} - name := naming.GenFileName(file, ".swagger.json") - for _, svc := range file.Service { - for _, meth := range svc.Method { - if !t.ShouldGenForMethod(file, svc, meth) { - continue - } - apiInfo := t.GetHttpInfoCached(file, svc, meth) - pathItem := swaggerPathItemObject{} - if originPathItem, ok := swaggerObj.Paths[apiInfo.Path]; ok { - pathItem = originPathItem - } - op := t.getOperationByHTTPMethod(apiInfo.HttpMethod, &pathItem) - op.Summary = apiInfo.Title - op.Description = apiInfo.Description - swaggerObj.Paths[apiInfo.Path] = pathItem - op.Tags = []string{pkg + "." + svc.GetName()} - - // request - request := t.Reg.MessageDefinition(meth.GetInputType()) - // request cannot represent by simple form - isComplexRequest := false - for _, field := range request.Descriptor.Field { - if !generator.IsScalar(field) { - isComplexRequest = true - break - } - } - if !isComplexRequest && apiInfo.HttpMethod == "GET" { - for _, field := range request.Descriptor.Field { - if !generator.IsScalar(field) { - continue - } - p := t.getQueryParameter(file, request, field) - op.Parameters = append(op.Parameters, p) - } - } else { - p := swaggerParameterObject{} - p.In = "body" - p.Required = true - p.Name = "body" - p.Schema = &swaggerSchemaObject{} - p.Schema.Ref = "#/definitions/" + meth.GetInputType() - op.Parameters = []swaggerParameterObject{p} - } - - // response - resp := swaggerResponseObject{} - resp.Description = "A successful response." - - // proto 里面的response只定义data里面的 - // 所以需要把code msg data 这一级加上 - resp.Schema.Type = "object" - resp.Schema.Properties = &swaggerSchemaObjectProperties{} - p := keyVal{Key: "code", Value: &schemaCore{Type: "integer"}} - *resp.Schema.Properties = append(*resp.Schema.Properties, p) - p = keyVal{Key: "message", Value: &schemaCore{Type: "string"}} - *resp.Schema.Properties = append(*resp.Schema.Properties, p) - p = keyVal{Key: "data", Value: schemaCore{Ref: "#/definitions/" + meth.GetOutputType()}} - *resp.Schema.Properties = append(*resp.Schema.Properties, p) - op.Responses = swaggerResponsesObject{"200": resp} - } - } - - // walk though definitions - t.walkThroughFileDefinition(file) - defs := swaggerDefinitionsObject{} - swaggerObj.Definitions = defs - for typ, msg := range t.defsMap { - def := swaggerSchemaObject{} - def.Properties = new(swaggerSchemaObjectProperties) - def.Description = strings.Trim(msg.Comments.Leading, "\n\r ") - for _, field := range msg.Descriptor.Field { - p := keyVal{Key: generator.GetFormOrJSONName(field)} - schema := t.schemaForField(file, msg, field) - if generator.GetFieldRequired(field, t.Reg, msg) { - def.Required = append(def.Required, p.Key) - } - p.Value = schema - *def.Properties = append(*def.Properties, p) - } - def.Type = "object" - defs[typ] = def - } - b, _ := json.MarshalIndent(swaggerObj, "", " ") - str := string(b) - out.Name = &name - out.Content = &str - return out -} - -func (t *swaggerGen) getOperationByHTTPMethod(httpMethod string, pathItem *swaggerPathItemObject) *swaggerOperationObject { - var op = &swaggerOperationObject{} - switch httpMethod { - case http.MethodGet: - pathItem.Get = op - case http.MethodPost: - pathItem.Post = op - case http.MethodPut: - pathItem.Put = op - case http.MethodDelete: - pathItem.Delete = op - case http.MethodPatch: - pathItem.Patch = op - default: - pathItem.Get = op - } - return op -} - -func (t *swaggerGen) getQueryParameter(file *descriptor.FileDescriptorProto, - input *typemap.MessageDefinition, - field *descriptor.FieldDescriptorProto) swaggerParameterObject { - p := swaggerParameterObject{} - p.Name = generator.GetFormOrJSONName(field) - fComment, _ := t.Reg.FieldComments(input, field) - cleanComment := tag.GetCommentWithoutTag(fComment.Leading) - - p.Description = strings.Trim(strings.Join(cleanComment, "\n"), "\n\r ") - validateComment := getValidateComment(field) - if p.Description != "" && validateComment != "" { - p.Description = p.Description + "," + validateComment - } else if validateComment != "" { - p.Description = validateComment - } - p.In = "query" - p.Required = generator.GetFieldRequired(field, t.Reg, input) - typ, isArray, format := getFieldSwaggerType(field) - if isArray { - p.Items = &swaggerItemsObject{} - p.Type = "array" - p.Items.Type = typ - p.Items.Format = format - } else { - p.Type = typ - p.Format = format - } - return p -} - -func (t *swaggerGen) schemaForField(file *descriptor.FileDescriptorProto, - msg *typemap.MessageDefinition, - field *descriptor.FieldDescriptorProto) swaggerSchemaObject { - schema := swaggerSchemaObject{} - fComment, err := t.Reg.FieldComments(msg, field) - if err != nil { - gen.Error(err, "comment not found err %+v") - } - schema.Description = strings.Trim(fComment.Leading, "\n\r ") - validateComment := getValidateComment(field) - if schema.Description != "" && validateComment != "" { - schema.Description = schema.Description + "," + validateComment - } else if validateComment != "" { - schema.Description = validateComment - } - typ, isArray, format := getFieldSwaggerType(field) - if !generator.IsScalar(field) { - if generator.IsMap(field, t.Reg) { - schema.Type = "object" - mapMsg := t.Reg.MessageDefinition(field.GetTypeName()) - mapValueField := mapMsg.Descriptor.Field[1] - valSchema := t.schemaForField(file, mapMsg, mapValueField) - schema.AdditionalProperties = &valSchema - } else { - if isArray { - schema.Items = &swaggerItemsObject{} - schema.Type = "array" - schema.Items.Ref = "#/definitions/" + field.GetTypeName() - } else { - schema.Ref = "#/definitions/" + field.GetTypeName() - } - } - } else { - if isArray { - schema.Items = &swaggerItemsObject{} - schema.Type = "array" - schema.Items.Type = typ - schema.Items.Format = format - } else { - schema.Type = typ - schema.Format = format - } - } - return schema -} - -func (t *swaggerGen) walkThroughFileDefinition(file *descriptor.FileDescriptorProto) { - for _, svc := range file.Service { - for _, meth := range svc.Method { - shouldGen := t.ShouldGenForMethod(file, svc, meth) - if !shouldGen { - continue - } - t.walkThroughMessages(t.Reg.MessageDefinition(meth.GetOutputType())) - t.walkThroughMessages(t.Reg.MessageDefinition(meth.GetInputType())) - } - } -} - -func (t *swaggerGen) walkThroughMessages(msg *typemap.MessageDefinition) { - _, ok := t.defsMap[msg.ProtoName()] - if ok { - return - } - if !msg.Descriptor.GetOptions().GetMapEntry() { - t.defsMap[msg.ProtoName()] = msg - } - for _, field := range msg.Descriptor.Field { - if field.GetType() == descriptor.FieldDescriptorProto_TYPE_MESSAGE { - t.walkThroughMessages(t.Reg.MessageDefinition(field.GetTypeName())) - } - } -} - -func getFieldSwaggerType(field *descriptor.FieldDescriptorProto) (typeName string, isArray bool, formatName string) { - typeName = "unknown" - switch field.GetType() { - case descriptor.FieldDescriptorProto_TYPE_BOOL: - typeName = "boolean" - case descriptor.FieldDescriptorProto_TYPE_DOUBLE: - typeName = "number" - formatName = "double" - case descriptor.FieldDescriptorProto_TYPE_FLOAT: - typeName = "number" - formatName = "float" - case - descriptor.FieldDescriptorProto_TYPE_INT64, - descriptor.FieldDescriptorProto_TYPE_UINT64, - descriptor.FieldDescriptorProto_TYPE_INT32, - descriptor.FieldDescriptorProto_TYPE_FIXED64, - descriptor.FieldDescriptorProto_TYPE_FIXED32, - descriptor.FieldDescriptorProto_TYPE_ENUM, - descriptor.FieldDescriptorProto_TYPE_UINT32, - descriptor.FieldDescriptorProto_TYPE_SFIXED32, - descriptor.FieldDescriptorProto_TYPE_SFIXED64, - descriptor.FieldDescriptorProto_TYPE_SINT32, - descriptor.FieldDescriptorProto_TYPE_SINT64: - typeName = "integer" - case - descriptor.FieldDescriptorProto_TYPE_STRING, - descriptor.FieldDescriptorProto_TYPE_BYTES: - typeName = "string" - case descriptor.FieldDescriptorProto_TYPE_MESSAGE: - typeName = "object" - } - if field.Label != nil && *field.Label == descriptor.FieldDescriptorProto_LABEL_REPEATED { - isArray = true - } - return -} - -func getValidateComment(field *descriptor.FieldDescriptorProto) string { - var ( - tags []reflect.StructTag - ) - //get required info from gogoproto.moretags - moretags := tag.GetMoreTags(field) - if moretags != nil { - tags = []reflect.StructTag{reflect.StructTag(*moretags)} - } - validateTag := tag.GetTagValue("validate", tags) - - // trim - regStr := []string{ - "required *,*", - "omitempty *,*", - } - for _, v := range regStr { - re, _ := regexp.Compile(v) - validateTag = re.ReplaceAllString(validateTag, "") - } - return validateTag -} diff --git a/tool/protobuf/protoc-gen-bswagger/main.go b/tool/protobuf/protoc-gen-bswagger/main.go deleted file mode 100644 index 45b68dec5..000000000 --- a/tool/protobuf/protoc-gen-bswagger/main.go +++ /dev/null @@ -1,22 +0,0 @@ -package main - -import ( - "flag" - "fmt" - "os" - - "github.com/go-kratos/kratos/tool/protobuf/pkg/gen" - "github.com/go-kratos/kratos/tool/protobuf/pkg/generator" -) - -func main() { - versionFlag := flag.Bool("version", false, "print version and exit") - flag.Parse() - if *versionFlag { - fmt.Println(generator.Version) - os.Exit(0) - } - - g := NewSwaggerGenerator() - gen.Main(g) -} diff --git a/tool/protobuf/protoc-gen-bswagger/types.go b/tool/protobuf/protoc-gen-bswagger/types.go deleted file mode 100644 index e6a7a866c..000000000 --- a/tool/protobuf/protoc-gen-bswagger/types.go +++ /dev/null @@ -1,218 +0,0 @@ -package main - -import ( - "bytes" - "encoding/json" -) - -// http://swagger.io/specification/#infoObject -type swaggerInfoObject struct { - Title string `json:"title"` - Description string `json:"description,omitempty"` - TermsOfService string `json:"termsOfService,omitempty"` - Version string `json:"version"` - - Contact *swaggerContactObject `json:"contact,omitempty"` - License *swaggerLicenseObject `json:"license,omitempty"` -} - -// http://swagger.io/specification/#contactObject -type swaggerContactObject struct { - Name string `json:"name,omitempty"` - URL string `json:"url,omitempty"` - Email string `json:"email,omitempty"` -} - -// http://swagger.io/specification/#licenseObject -type swaggerLicenseObject struct { - Name string `json:"name,omitempty"` - URL string `json:"url,omitempty"` -} - -// http://swagger.io/specification/#externalDocumentationObject -type swaggerExternalDocumentationObject struct { - Description string `json:"description,omitempty"` - URL string `json:"url,omitempty"` -} - -// http://swagger.io/specification/#swaggerObject -type swaggerObject struct { - Swagger string `json:"swagger"` - Info swaggerInfoObject `json:"info"` - Host string `json:"host,omitempty"` - BasePath string `json:"basePath,omitempty"` - Schemes []string `json:"schemes"` - Consumes []string `json:"consumes"` - Produces []string `json:"produces"` - Paths swaggerPathsObject `json:"paths"` - Definitions swaggerDefinitionsObject `json:"definitions"` - StreamDefinitions swaggerDefinitionsObject `json:"x-stream-definitions,omitempty"` - SecurityDefinitions swaggerSecurityDefinitionsObject `json:"securityDefinitions,omitempty"` - Security []swaggerSecurityRequirementObject `json:"security,omitempty"` - ExternalDocs *swaggerExternalDocumentationObject `json:"externalDocs,omitempty"` -} - -// http://swagger.io/specification/#securityDefinitionsObject -type swaggerSecurityDefinitionsObject map[string]swaggerSecuritySchemeObject - -// http://swagger.io/specification/#securitySchemeObject -type swaggerSecuritySchemeObject struct { - Type string `json:"type"` - Description string `json:"description,omitempty"` - Name string `json:"name,omitempty"` - In string `json:"in,omitempty"` - Flow string `json:"flow,omitempty"` - AuthorizationURL string `json:"authorizationUrl,omitempty"` - TokenURL string `json:"tokenUrl,omitempty"` - Scopes swaggerScopesObject `json:"scopes,omitempty"` -} - -// http://swagger.io/specification/#scopesObject -type swaggerScopesObject map[string]string - -// http://swagger.io/specification/#securityRequirementObject -type swaggerSecurityRequirementObject map[string][]string - -// http://swagger.io/specification/#pathsObject -type swaggerPathsObject map[string]swaggerPathItemObject - -// http://swagger.io/specification/#pathItemObject -type swaggerPathItemObject struct { - Get *swaggerOperationObject `json:"get,omitempty"` - Delete *swaggerOperationObject `json:"delete,omitempty"` - Post *swaggerOperationObject `json:"post,omitempty"` - Put *swaggerOperationObject `json:"put,omitempty"` - Patch *swaggerOperationObject `json:"patch,omitempty"` -} - -// http://swagger.io/specification/#operationObject -type swaggerOperationObject struct { - Summary string `json:"summary,omitempty"` - Description string `json:"description,omitempty"` - OperationID string `json:"operationId,omitempty"` - Responses swaggerResponsesObject `json:"responses"` - Parameters swaggerParametersObject `json:"parameters,omitempty"` - Tags []string `json:"tags,omitempty"` - Deprecated bool `json:"deprecated,omitempty"` - - Security *[]swaggerSecurityRequirementObject `json:"security,omitempty"` - ExternalDocs *swaggerExternalDocumentationObject `json:"externalDocs,omitempty"` -} - -type swaggerParametersObject []swaggerParameterObject - -// http://swagger.io/specification/#parameterObject -type swaggerParameterObject struct { - Name string `json:"name"` - Description string `json:"description,omitempty"` - In string `json:"in,omitempty"` - Required bool `json:"required"` - Type string `json:"type,omitempty"` - Format string `json:"format,omitempty"` - Items *swaggerItemsObject `json:"items,omitempty"` - Enum []string `json:"enum,omitempty"` - CollectionFormat string `json:"collectionFormat,omitempty"` - Default string `json:"default,omitempty"` - MinItems *int `json:"minItems,omitempty"` - - // Or you can explicitly refer to another type. If this is defined all - // other fields should be empty - Schema *swaggerSchemaObject `json:"schema,omitempty"` -} - -// core part of schema, which is common to itemsObject and schemaObject. -// http://swagger.io/specification/#itemsObject -type schemaCore struct { - Type string `json:"type,omitempty"` - Format string `json:"format,omitempty"` - Ref string `json:"$ref,omitempty"` - Example json.RawMessage `json:"example,omitempty"` - - Items *swaggerItemsObject `json:"items,omitempty"` - - // If the item is an enumeration include a list of all the *NAMES* of the - // enum values. I'm not sure how well this will work but assuming all enums - // start from 0 index it will be great. I don't think that is a good assumption. - Enum []string `json:"enum,omitempty"` - Default string `json:"default,omitempty"` -} - -type swaggerItemsObject schemaCore - -func (o *swaggerItemsObject) getType() string { - if o == nil { - return "" - } - return o.Type -} - -// http://swagger.io/specification/#responsesObject -type swaggerResponsesObject map[string]swaggerResponseObject - -// http://swagger.io/specification/#responseObject -type swaggerResponseObject struct { - Description string `json:"description"` - Schema swaggerSchemaObject `json:"schema"` -} - -type keyVal struct { - Key string - Value interface{} -} - -type swaggerSchemaObjectProperties []keyVal - -func (op swaggerSchemaObjectProperties) MarshalJSON() ([]byte, error) { - var buf bytes.Buffer - buf.WriteString("{") - for i, kv := range op { - if i != 0 { - buf.WriteString(",") - } - key, err := json.Marshal(kv.Key) - if err != nil { - return nil, err - } - buf.Write(key) - buf.WriteString(":") - val, err := json.Marshal(kv.Value) - if err != nil { - return nil, err - } - buf.Write(val) - } - - buf.WriteString("}") - return buf.Bytes(), nil -} - -// http://swagger.io/specification/#schemaObject -type swaggerSchemaObject struct { - schemaCore - // Properties can be recursively defined - Properties *swaggerSchemaObjectProperties `json:"properties,omitempty"` - AdditionalProperties *swaggerSchemaObject `json:"additionalProperties,omitempty"` - - Description string `json:"description,omitempty"` - Title string `json:"title,omitempty"` - - ExternalDocs *swaggerExternalDocumentationObject `json:"externalDocs,omitempty"` - - MultipleOf float64 `json:"multipleOf,omitempty"` - Maximum float64 `json:"maximum,omitempty"` - ExclusiveMaximum bool `json:"exclusiveMaximum,omitempty"` - Minimum float64 `json:"minimum,omitempty"` - ExclusiveMinimum bool `json:"exclusiveMinimum,omitempty"` - MaxLength uint64 `json:"maxLength,omitempty"` - MinLength uint64 `json:"minLength,omitempty"` - Pattern string `json:"pattern,omitempty"` - MaxItems uint64 `json:"maxItems,omitempty"` - MinItems uint64 `json:"minItems,omitempty"` - UniqueItems bool `json:"uniqueItems,omitempty"` - MaxProperties uint64 `json:"maxProperties,omitempty"` - MinProperties uint64 `json:"minProperties,omitempty"` - Required []string `json:"required,omitempty"` -} - -// http://swagger.io/specification/#definitionsObject -type swaggerDefinitionsObject map[string]swaggerSchemaObject diff --git a/tool/protobuf/protoc-gen-ecode/generator/generator.go b/tool/protobuf/protoc-gen-ecode/generator/generator.go deleted file mode 100644 index e65487bff..000000000 --- a/tool/protobuf/protoc-gen-ecode/generator/generator.go +++ /dev/null @@ -1,118 +0,0 @@ -package generator - -import ( - "strconv" - "strings" - - "github.com/golang/protobuf/proto" - "github.com/golang/protobuf/protoc-gen-go/descriptor" - plugin "github.com/golang/protobuf/protoc-gen-go/plugin" - - "github.com/go-kratos/kratos/tool/protobuf/pkg/generator" - "github.com/go-kratos/kratos/tool/protobuf/pkg/naming" -) - -type ecode struct { - generator.Base - filesHandled int -} - -// EcodeGenerator ecode generator. -func EcodeGenerator() *ecode { - t := &ecode{} - return t -} - -// Generate ... -func (t *ecode) Generate(in *plugin.CodeGeneratorRequest) *plugin.CodeGeneratorResponse { - t.Setup(in) - - // Showtime! Generate the response. - resp := new(plugin.CodeGeneratorResponse) - for _, f := range t.GenFiles { - respFile := t.generateForFile(f) - if respFile != nil { - resp.File = append(resp.File, respFile) - } - } - return resp -} - -func (t *ecode) generateForFile(file *descriptor.FileDescriptorProto) *plugin.CodeGeneratorResponse_File { - var enums []*descriptor.EnumDescriptorProto - for _, enum := range file.EnumType { - if strings.HasSuffix(*enum.Name, "ErrCode") { - enums = append(enums, enum) - } - } - if len(enums) == 0 { - return nil - } - resp := new(plugin.CodeGeneratorResponse_File) - t.generateFileHeader(file, t.GenPkgName) - t.generateImports(file) - for _, enum := range enums { - t.generateEcode(file, enum) - } - - resp.Name = proto.String(naming.GenFileName(file, ".ecode.go")) - resp.Content = proto.String(t.FormattedOutput()) - t.Output.Reset() - - t.filesHandled++ - return resp -} - -func (t *ecode) generateFileHeader(file *descriptor.FileDescriptorProto, pkgName string) { - t.P("// Code generated by protoc-gen-ecode ", generator.Version, ", DO NOT EDIT.") - t.P("// source: ", file.GetName()) - t.P() - if t.filesHandled == 0 { - comment, err := t.Reg.FileComments(file) - if err == nil && comment.Leading != "" { - // doc for the first file - t.P("/*") - t.P("Package ", t.GenPkgName, " is a generated ecode package.") - t.P("This code was generated with kratos/tool/protobuf/protoc-gen-ecode ", generator.Version, ".") - t.P() - for _, line := range strings.Split(comment.Leading, "\n") { - line = strings.TrimPrefix(line, " ") - // ensure we don't escape from the block comment - line = strings.Replace(line, "*/", "* /", -1) - t.P(line) - } - t.P() - t.P("It is generated from these files:") - for _, f := range t.GenFiles { - t.P("\t", f.GetName()) - } - t.P("*/") - } - } - t.P(`package `, pkgName) - t.P() -} - -func (t *ecode) generateImports(file *descriptor.FileDescriptorProto) { - t.P(`import (`) - t.P(` "github.com/go-kratos/kratos/pkg/ecode"`) - t.P(`)`) - t.P() - t.P(`// to suppressed 'imported but not used warning'`) - t.P(`var _ ecode.Codes`) -} - -func (t *ecode) generateEcode(file *descriptor.FileDescriptorProto, enum *descriptor.EnumDescriptorProto) { - t.P("// ", *enum.Name, " ecode") - t.P("var (") - - for _, item := range enum.Value { - if *item.Number == 0 { - continue - } - // NOTE: eg: t.P("UserNotExist = New(-404) ") - t.P(*item.Name, " = ", "ecode.New(", strconv.Itoa(int(*item.Number)), ")") - } - - t.P(")") -} diff --git a/tool/protobuf/protoc-gen-ecode/generator/generator_test.go b/tool/protobuf/protoc-gen-ecode/generator/generator_test.go deleted file mode 100644 index 35e1a9240..000000000 --- a/tool/protobuf/protoc-gen-ecode/generator/generator_test.go +++ /dev/null @@ -1,27 +0,0 @@ -package generator - -import ( - "os" - "os/exec" - "testing" - - "github.com/golang/protobuf/proto" - plugin "github.com/golang/protobuf/protoc-gen-go/plugin" -) - -func TestGenerateParseCommandLineParamsError(t *testing.T) { - if os.Getenv("BE_CRASHER") == "1" { - g := &ecode{} - g.Generate(&plugin.CodeGeneratorRequest{ - Parameter: proto.String("invalid"), - }) - return - } - cmd := exec.Command(os.Args[0], "-test.run=TestGenerateParseCommandLineParamsError") - cmd.Env = append(os.Environ(), "BE_CRASHER=1") - err := cmd.Run() - if e, ok := err.(*exec.ExitError); ok && !e.Success() { - return - } - t.Fatalf("process ran with err %v, want exit status 1", err) -} diff --git a/tool/protobuf/protoc-gen-ecode/main.go b/tool/protobuf/protoc-gen-ecode/main.go deleted file mode 100644 index 7c408bed9..000000000 --- a/tool/protobuf/protoc-gen-ecode/main.go +++ /dev/null @@ -1,23 +0,0 @@ -package main - -import ( - "flag" - "fmt" - "os" - - "github.com/go-kratos/kratos/tool/protobuf/pkg/gen" - "github.com/go-kratos/kratos/tool/protobuf/pkg/generator" - ecodegen "github.com/go-kratos/kratos/tool/protobuf/protoc-gen-ecode/generator" -) - -func main() { - versionFlag := flag.Bool("version", false, "print version and exit") - flag.Parse() - if *versionFlag { - fmt.Println(generator.Version) - os.Exit(0) - } - - g := ecodegen.EcodeGenerator() - gen.Main(g) -} diff --git a/tool/testcli/README.md b/tool/testcli/README.md deleted file mode 100644 index 3b56fcad6..000000000 --- a/tool/testcli/README.md +++ /dev/null @@ -1,154 +0,0 @@ -## testcli UT运行环境构建工具 -基于 docker-compose 实现跨平台跨语言环境的容器依赖管理方案,以解决运行ut场景下的 (mysql, redis, mc)容器依赖问题。 - -*这个是testing/lich的二进制工具版本(Go请直接使用库版本:github.com/go-kratos/kratos/pkg/testing/lich)* - -### 功能和特性 -- 自动读取 test 目录下的 yaml 并启动依赖 -- 自动导入 test 目录下的 DB 初始化 SQL -- 提供特定容器内的 healthcheck (mysql, mc, redis) -- 提供一站式解决 UT 服务依赖的工具版本 (testcli) - -### 编译安装 -*使用本工具/库需要前置安装好 docker & docker-compose@v1.24.1^* - -#### Method 1. With go get -```shell -go get -u github.com/go-kratos/kratos/tool/testcli -$GOPATH/bin/testcli -h -``` -#### Method 2. Build with Go -```shell -cd github.com/go-kratos/kratos/tool/testcli -go build -o $GOPATH/bin/testcli -$GOPATH/bin/testcli -h -``` -#### Method 3. Import with Kratos pkg -```Go -import "github.com/go-kratos/kratos/pkg/testing/lich" -``` - -### 构建数据 -#### Step 1. create docker-compose.yml -创建依赖服务的 docker-compose.yml,并把它放在项目路径下的 test 文件夹下面。例如: -```shell -mkdir -p $YOUR_PROJECT/test -``` -```yaml -version: "3.7" - -services: - db: - image: mysql:5.6 - ports: - - 3306:3306 - environment: - - MYSQL_ROOT_PASSWORD=root - volumes: - - .:/docker-entrypoint-initdb.d - command: [ - '--character-set-server=utf8', - '--collation-server=utf8_unicode_ci' - ] - - redis: - image: redis - ports: - - 6379:6379 -``` -一般来讲,我们推荐在项目根目录创建 test 目录,里面存放描述服务的yml,以及需要初始化的数据(database.sql等)。 - -同时也需要注意,正确的对容器内服务进行健康检测,testcli会在容器的health状态执行UT,其实我们也内置了针对几个较为通用镜像(mysql mariadb mc redis)的健康检测,也就是不写也没事(^^;; - -#### Step 2. export database.sql -构造初始化的数据(database.sql等),当然也把它也在 test 文件夹里。 -```sql -CREATE DATABASE IF NOT EXISTS `YOUR_DATABASE_NAME`; - -SET NAMES 'utf8'; -USE `YOUR_DATABASE_NAME`; - -CREATE TABLE IF NOT EXISTS `YOUR_TABLE_NAME` ( - `id` int(11) unsigned NOT NULL AUTO_INCREMENT COMMENT '主键', - PRIMARY KEY (`id`), -) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='YOUR_TABLE_NAME'; -``` -这里需要注意,在创建库/表的时候尽量加上 IF NOT EXISTS,以给予一定程度的容错,以及 SET NAMES 'utf8'; 用于解决客户端连接乱码问题。 - -#### Step 3. change your project mysql config -```toml -[mysql] - addr = "127.0.0.1:3306" - dsn = "root:root@tcp(127.0.0.1:3306)/YOUR_DATABASE?timeout=1s&readTimeout=1s&writeTimeout=1s&parseTime=true&loc=Local&charset=utf8mb4,utf8" - active = 20 - idle = 10 - idleTimeout ="1s" - queryTimeout = "1s" - execTimeout = "1s" - tranTimeout = "1s" -``` -在 *Step 1* 我们已经指定了服务对外暴露的端口为3306(这当然也可以是你指定的任何值),那理所应当的我们也要修改项目连接数据库的配置~ - -Great! 至此你已经完成了运行所需要用到的数据配置,接下来就来运行它。 - -### 运行 -开头也说过本工具支持两种运行方式:testcli 二进制工具版本和 go package 源码包,业务方可以根据需求场景进行选择。 -#### Method 1. With testcli tool -*已支持的 flag: -f,--nodown,down,run* -- -f,指定 docker-compose.yaml 文件路径,默认为当前目录下。 -- --nodown,指定是否在UT执行完成后保留容器,以供下次复用。 -- down,teardown 销毁当前项目下这个 compose 文件产生的容器。 -- run,运行你当前语言的单测执行命令(如:golang为 go test -v ./) - -example: -```shell -testcli -f ../../test/docker-compose.yaml run go test -v ./ -``` -#### Method 2. Import with Kratos pkg -- Step1. 在 Dao|Service 层中的 TestMain 单测主入口中,import "github.com/go-kratos/kratos/pkg/testing/lich" 引入testcli工具的go库版本。 -- Step2. 使用 flag.Set("f", "../../test/docker-compose.yaml") 指定 docker-compose.yaml 文件的路径。 -- Step3. 在 flag.Parse() 后即可使用 lich.Setup() 安装依赖&初始化数据(注意测试用例执行结束后 lich.Teardown() 回收下~) -- Step4. 运行 `go test -v ./ `看看效果吧~ - -example: -```Go -package dao - - -import ( - "flag" - "os" - "strings" - "testing" - - "github.com/go-kratos/kratos/pkg/conf/paladin" - "github.com/go-kratos/kratos/pkg/testing/lich" - ) - -var ( - d *Dao -) - -func TestMain(m *testing.M) { - flag.Set("conf", "../../configs") - flag.Set("f", "../../test/docker-compose.yaml") - flag.Parse() - if err := paladin.Init(); err != nil { - panic(err) - } - if err := lich.Setup(); err != nil { - panic(err) - } - defer lich.Teardown() - d = New() - if code := m.Run(); code != 0 { - panic(code) - } -} - ``` -## 注意 -因为启动mysql容器较为缓慢,健康检测的机制会重试3次,每次暂留5秒钟,基本在10s内mysql就能从creating到服务正常启动! - -当然你也可以在使用 testcli 时加上 --nodown,使其不用每次跑都新建容器,只在第一次跑的时候会初始化容器,后面都进行复用,这样速度会快很多。 - -成功启动后就欢乐奔放的玩耍吧~ Good Luck! diff --git a/tool/testcli/docker-compose.yaml b/tool/testcli/docker-compose.yaml deleted file mode 100644 index 57f78dac8..000000000 --- a/tool/testcli/docker-compose.yaml +++ /dev/null @@ -1,26 +0,0 @@ -version: "3.7" - -services: - db: - image: mysql:5.6 - ports: - - 3306:3306 - environment: - - MYSQL_ROOT_PASSWORD=root - - TZ=Asia/Shanghai - volumes: - - .:/docker-entrypoint-initdb.d - command: [ - '--character-set-server=utf8', - '--collation-server=utf8_unicode_ci' - ] - - redis: - image: redis - ports: - - 6379:6379 - - memcached: - image: memcached - ports: - - 11211:11211 \ No newline at end of file diff --git a/tool/testcli/main.go b/tool/testcli/main.go deleted file mode 100644 index 7efde6ca5..000000000 --- a/tool/testcli/main.go +++ /dev/null @@ -1,50 +0,0 @@ -package main - -import ( - "flag" - "os" - "os/exec" - "strings" - - "github.com/go-kratos/kratos/pkg/testing/lich" -) - -func parseArgs() (flags map[string]string) { - flags = make(map[string]string) - for idx, arg := range os.Args { - if idx == 0 { - continue - } - if arg == "down" { - flags["down"] = "" - return - } - if cmds := os.Args[idx+1:]; arg == "run" { - flags["run"] = strings.Join(cmds, " ") - return - } - } - return -} - -func main() { - flag.Parse() - flags := parseArgs() - if _, ok := flags["down"]; ok { - lich.Teardown() - return - } - if cmd, ok := flags["run"]; !ok || cmd == "" { - panic("Your need 'run' flag assign to be run commands.") - } - if err := lich.Setup(); err != nil { - panic(err) - } - defer lich.Teardown() - cmds := strings.Split(flags["run"], " ") - cmd := exec.Command(cmds[0], cmds[1:]...) - cmd.Stdout, cmd.Stderr = os.Stdout, os.Stderr - if err := cmd.Run(); err != nil { - panic(err) - } -} diff --git a/tool/testgen/README.md b/tool/testgen/README.md deleted file mode 100644 index 890992ae8..000000000 --- a/tool/testgen/README.md +++ /dev/null @@ -1,52 +0,0 @@ -## testgen UT代码自动生成器 -解放你的双手,让你的UT一步到位! - -### 功能和特性 -- 支持生成 Dao|Service 层UT代码功能(每个方法包含一个正向用例) -- 支持生成 Dao|Service 层测试入口文件dao_test.go, service_test.go(用于控制初始化,控制测试流程等) -- 支持生成Mock代码(使用GoMock框架) -- 支持选择不同模式生成不同代码(使用"–m mode"指定) -- 生成单元测试代码时,同时支持传入目录或文件 -- 支持指定方法追加生成测试用例(使用"–func funcName"指定) - -### 编译安装 -#### Method 1. With go get -```shell -go get -u github.com/go-kratos/kratos/tool/testgen -$GOPATH/bin/testgen -h -``` -#### Method 2. Build with Go -```shell -cd github.com/go-kratos/kratos/tool/testgen -go build -o $GOPATH/bin/testgen -$GOPATH/bin/testgen -h -``` -### 运行 -#### 生成Dao/Service层单元UT -```shell -$GOPATH/bin/testgen YOUR_PROJECT/dao # default mode -$GOPATH/bin/testgen --m test path/to/your/pkg -$GOPATH/bin/testgen --func functionName path/to/your/pkg -``` - -#### 生成接口类型 -```shell -$GOPATH/bin/testgen --m interface YOUR_PROJECT/dao #当前仅支持传目录,如目录包含子目录也会做处理 -``` - -#### 生成Mock代码 - ```shell -$GOPATH/bin/testgen --m mock YOUR_PROJECT/dao #仅传入包路径即可 -``` - -#### 生成Monkey代码 -```shell -$GOPATH/bin/testgen --m monkey yourCodeDirPath #仅传入包路径即可 -``` -### 赋诗一首 -``` -莫生气 莫生气 -代码辣鸡非我意 -自己动手分田地 -谈笑风生活长命 -``` \ No newline at end of file diff --git a/tool/testgen/gen.go b/tool/testgen/gen.go deleted file mode 100644 index b515fda29..000000000 --- a/tool/testgen/gen.go +++ /dev/null @@ -1,419 +0,0 @@ -package main - -import ( - "bytes" - "fmt" - "io/ioutil" - "os" - "path/filepath" - "strings" - - "github.com/otokaze/mock/mockgen" - "github.com/otokaze/mock/mockgen/model" -) - -func genTest(parses []*parse) (err error) { - for _, p := range parses { - switch { - case strings.HasSuffix(p.Path, "_mock.go") || - strings.HasSuffix(p.Path, ".intf.go"): - continue - case strings.HasSuffix(p.Path, "dao.go") || - strings.HasSuffix(p.Path, "service.go"): - err = p.genTestMain() - default: - err = p.genUTTest() - } - if err != nil { - break - } - } - return -} - -func (p *parse) genUTTest() (err error) { - var ( - buffer bytes.Buffer - impts = strings.Join([]string{ - `"context"`, - `"testing"`, - `. "github.com/smartystreets/goconvey/convey"`, - }, "\n\t") - content []byte - ) - filename := strings.Replace(p.Path, ".go", "_test.go", -1) - if _, err = os.Stat(filename); (_func == "" && err == nil) || - (err != nil && os.IsExist(err)) { - err = nil - return - } - for _, impt := range p.Imports { - impts += "\n\t\"" + impt.V + "\"" - } - if _func == "" { - buffer.WriteString(fmt.Sprintf(tpPackage, p.Package)) - buffer.WriteString(fmt.Sprintf(tpImport, impts)) - } - for _, parseFunc := range p.Funcs { - if _func != "" && _func != parseFunc.Name { - continue - } - var ( - methodK string - tpVars string - vars []string - val []string - notice = "Then " - reset string - ) - if method := ConvertMethod(p.Path); method != "" { - methodK = method + "." - } - tpTestFuncs := fmt.Sprintf(tpTestFunc, strings.Title(p.Package), parseFunc.Name, "", parseFunc.Name, "%s", "%s", "%s") - tpTestFuncBeCall := methodK + parseFunc.Name + "(%s)\n\t\t\tConvey(\"%s\", func() {" - if parseFunc.Result == nil { - tpTestFuncBeCall = fmt.Sprintf(tpTestFuncBeCall, "%s", "No return values") - tpTestFuncs = fmt.Sprintf(tpTestFuncs, "%s", tpTestFuncBeCall, "%s") - } - for k, res := range parseFunc.Result { - if res.K == "" { - res.K = fmt.Sprintf("p%d", k+1) - } - var so string - if res.V == "error" { - res.K = "err" - so = fmt.Sprintf("\tSo(%s, ShouldBeNil)", res.K) - notice += "err should be nil." - } else { - so = fmt.Sprintf("\tSo(%s, ShouldNotBeNil)", res.K) - val = append(val, res.K) - } - if len(parseFunc.Result) <= k+1 { - if len(val) != 0 { - notice += strings.Join(val, ",") + " should not be nil." - } - tpTestFuncBeCall = fmt.Sprintf(tpTestFuncBeCall, "%s", notice) - res.K += " := " + tpTestFuncBeCall - } else { - res.K += ", %s" - } - tpTestFuncs = fmt.Sprintf(tpTestFuncs, "%s", res.K+"\n\t\t\t%s", "%s") - tpTestFuncs = fmt.Sprintf(tpTestFuncs, "%s", "%s", so, "%s") - } - if parseFunc.Params == nil { - tpTestFuncs = fmt.Sprintf(tpTestFuncs, "%s", "", "%s") - } - for k, pType := range parseFunc.Params { - if pType.K == "" { - pType.K = fmt.Sprintf("a%d", k+1) - } - var ( - init string - params = pType.K - ) - switch { - case strings.HasPrefix(pType.V, "context"): - init = params + " = context.Background()" - case strings.HasPrefix(pType.V, "[]byte"): - init = params + " = " + pType.V + "(\"\")" - case strings.HasPrefix(pType.V, "[]"): - init = params + " = " + pType.V + "{}" - case strings.HasPrefix(pType.V, "int") || - strings.HasPrefix(pType.V, "uint") || - strings.HasPrefix(pType.V, "float") || - strings.HasPrefix(pType.V, "double"): - init = params + " = " + pType.V + "(0)" - case strings.HasPrefix(pType.V, "string"): - init = params + " = \"\"" - case strings.Contains(pType.V, "*xsql.Tx"): - init = params + ",_ = " + methodK + "BeginTran(c)" - reset += "\n\t" + params + ".Commit()" - case strings.HasPrefix(pType.V, "*"): - init = params + " = " + strings.Replace(pType.V, "*", "&", -1) + "{}" - case strings.Contains(pType.V, "chan"): - init = params + " = " + pType.V - case pType.V == "time.Time": - init = params + " = time.Now()" - case strings.Contains(pType.V, "chan"): - init = params + " = " + pType.V - default: - init = params + " " + pType.V - } - vars = append(vars, "\t\t"+init) - if len(parseFunc.Params) > k+1 { - params += ", %s" - } - tpTestFuncs = fmt.Sprintf(tpTestFuncs, "%s", params, "%s") - } - if len(vars) > 0 { - tpVars = fmt.Sprintf(tpVar, strings.Join(vars, "\n\t")) - } - tpTestFuncs = fmt.Sprintf(tpTestFuncs, tpVars, "%s") - if reset != "" { - tpTestResets := fmt.Sprintf(tpTestReset, reset) - tpTestFuncs = fmt.Sprintf(tpTestFuncs, tpTestResets) - } else { - tpTestFuncs = fmt.Sprintf(tpTestFuncs, "") - } - buffer.WriteString(tpTestFuncs) - } - var ( - file *os.File - flag = os.O_RDWR | os.O_CREATE | os.O_APPEND - ) - if file, err = os.OpenFile(filename, flag, 0644); err != nil { - return - } - if _func == "" { - content, _ = GoImport(filename, buffer.Bytes()) - } else { - content = buffer.Bytes() - } - if _, err = file.Write(content); err != nil { - return - } - if err = file.Close(); err != nil { - return - } - return -} - -func (p *parse) genTestMain() (err error) { - var ( - new bool - buffer bytes.Buffer - impts string - vars, mainFunc string - content []byte - instance, confFunc string - tomlPath = "**PUT PATH TO YOUR CONFIG FILES HERE**" - filename = strings.Replace(p.Path, ".go", "_test.go", -1) - ) - if p.Imports["paladin"] != nil { - new = true - } - // if _intfMode { - // imptsList = append(imptsList, `"github.com/golang/mock/gomock"`) - // for _, field := range p.Structs { - // var hit bool - // pkgName := strings.Split(field.V, ".")[0] - // interfaceName := strings.Split(field.V, ".")[1] - // if p.Imports[pkgName] != nil { - // if hit, err = checkInterfaceMock(strings.Split(field.V, ".")[1], p.Imports[pkgName].V); err != nil { - // return - // } - // } - // if hit { - // imptsList = append(imptsList, "mock"+p.Imports[pkgName].K+" \""+p.Imports[pkgName].V+"/mock\"") - // pkgName = "mock" + strings.Title(pkgName) - // interfaceName = "Mock" + interfaceName - // varsList = append(varsList, "mock"+strings.Title(field.K)+" *"+pkgName+"."+interfaceName) - // mockStmt += "\tmock" + strings.Title(field.K) + " = " + pkgName + ".New" + interfaceName + "(mockCtrl)\n" - // newStmt += "\t\t" + field.K + ":\tmock" + strings.Title(field.K) + ",\n" - // } else { - // pkgName = subString(field.V, "*", ".") - // if p.Imports[pkgName] != nil && pkgName != "conf" { - // imptsList = append(imptsList, p.Imports[pkgName].K+" \""+p.Imports[pkgName].V+"\"") - // } - // switch { - // case strings.HasPrefix(field.V, "*conf."): - // newStmt += "\t\t" + field.K + ":\tconf.Conf,\n" - // case strings.HasPrefix(field.V, "*"): - // newStmt += "\t\t" + field.K + ":\t" + strings.Replace(field.V, "*", "&", -1) + "{},\n" - // default: - // newStmt += "\t\t" + field.K + ":\t" + field.V + ",\n" - // } - // } - // } - // mockStmt = fmt.Sprintf(_tpTestServiceMainMockStmt, mockStmt) - // newStmt = fmt.Sprintf(_tpTestServiceMainNewStmt, newStmt) - // } - if instance = ConvertMethod(p.Path); instance == "s" { - vars = strings.Join([]string{"s *Service"}, "\n\t") - mainFunc = tpTestServiceMain - } else { - vars = strings.Join([]string{"d *Dao"}, "\n\t") - mainFunc = tpTestDaoMain - } - if new { - impts = strings.Join([]string{`"os"`, `"flag"`, `"testing"`, p.Imports["paladin"].V}, "\n\t") - confFunc = fmt.Sprintf(tpTestMainNew, instance+" = New()") - } else { - impts = strings.Join(append([]string{`"os"`, `"flag"`, `"testing"`}), "\n\t") - confFunc = fmt.Sprintf(tpTestMainOld, instance+" = New(conf.Conf)") - } - if _, err := os.Stat(filename); os.IsNotExist(err) { - buffer.WriteString(fmt.Sprintf(tpPackage, p.Package)) - buffer.WriteString(fmt.Sprintf(tpImport, impts)) - buffer.WriteString(fmt.Sprintf(tpVar, vars)) - buffer.WriteString(fmt.Sprintf(mainFunc, tomlPath, confFunc)) - content, _ = GoImport(filename, buffer.Bytes()) - ioutil.WriteFile(filename, content, 0644) - } - return -} - -func genInterface(parses []*parse) (err error) { - var ( - parse *parse - pkg = make(map[string]string) - ) - for _, parse = range parses { - if strings.Contains(parse.Path, ".intf.go") { - continue - } - dirPath := filepath.Dir(parse.Path) - for _, parseFunc := range parse.Funcs { - if (parseFunc.Method == nil) || - !(parseFunc.Name[0] >= 'A' && parseFunc.Name[0] <= 'Z') { - continue - } - var ( - params string - results string - ) - for k, param := range parseFunc.Params { - params += param.K + " " + param.P + param.V - if len(parseFunc.Params) > k+1 { - params += ", " - } - } - for k, res := range parseFunc.Result { - results += res.K + " " + res.P + res.V - if len(parseFunc.Result) > k+1 { - results += ", " - } - } - if len(results) != 0 { - results = "(" + results + ")" - } - pkg[dirPath] += "\t" + fmt.Sprintf(tpIntfcFunc, parseFunc.Name, params, results) - } - } - for k, v := range pkg { - var buffer bytes.Buffer - pathSplit := strings.Split(k, "/") - filename := k + "/" + pathSplit[len(pathSplit)-1] + ".intf.go" - if _, exist := os.Stat(filename); os.IsExist(exist) { - continue - } - buffer.WriteString(fmt.Sprintf(tpPackage, pathSplit[len(pathSplit)-1])) - buffer.WriteString(fmt.Sprintf(tpInterface, strings.Title(pathSplit[len(pathSplit)-1]), v)) - content, _ := GoImport(filename, buffer.Bytes()) - err = ioutil.WriteFile(filename, content, 0644) - } - return -} - -func genMock(files ...string) (err error) { - for _, file := range files { - var pkg *model.Package - if pkg, err = mockgen.ParseFile(file); err != nil { - return - } - if len(pkg.Interfaces) == 0 { - continue - } - var mockDir = pkg.SrcDir + "/mock" - if _, err = os.Stat(mockDir); os.IsNotExist(err) { - err = nil - os.Mkdir(mockDir, 0744) - } - var mockPath = mockDir + "/" + pkg.Name + "_mock.go" - if _, exist := os.Stat(mockPath); os.IsExist(exist) { - continue - } - var g = &mockgen.Generator{Filename: file} - if err = g.Generate(pkg, "mock", mockPath); err != nil { - return - } - if err = ioutil.WriteFile(mockPath, g.Output(), 0644); err != nil { - return - } - } - return -} - -func genMonkey(parses []*parse) (err error) { - var ( - pkg = make(map[string]string) - ) - for _, parse := range parses { - if strings.Contains(parse.Path, "monkey.go") || - strings.Contains(parse.Path, "/mock/") { - continue - } - var ( - path = strings.Split(filepath.Dir(parse.Path), "/") - pack = ConvertHump(path[len(path)-1]) - refer = path[len(path)-1] - mockVar, mockType, srcDir string - ) - for i := len(path) - 1; i > len(path)-4; i-- { - if path[i] == "dao" || path[i] == "service" { - srcDir = strings.Join(path[:i+1], "/") - break - } - pack = ConvertHump(path[i-1]) + pack - } - if mockVar = ConvertMethod(parse.Path); mockType == "d" { - mockType = "*" + refer + ".Dao" - } else { - mockType = "*" + refer + ".Service" - } - for _, parseFunc := range parse.Funcs { - if (parseFunc.Method == nil) || (parseFunc.Result == nil) || - !(parseFunc.Name[0] >= 'A' && parseFunc.Name[0] <= 'Z') { - continue - } - var ( - funcParams, funcResults, mockKey, mockValue, funcName string - ) - funcName = pack + parseFunc.Name - for k, param := range parseFunc.Params { - funcParams += "_ " + param.V - if len(parseFunc.Params) > k+1 { - funcParams += ", " - } - } - for k, res := range parseFunc.Result { - if res.K == "" { - if res.V == "error" { - res.K = "err" - } else { - res.K = fmt.Sprintf("p%d", k+1) - } - } - mockKey += res.K - mockValue += res.V - funcResults += res.K + " " + res.P + res.V - if len(parseFunc.Result) > k+1 { - mockKey += ", " - mockValue += ", " - funcResults += ", " - } - } - pkg[srcDir+"."+refer] += fmt.Sprintf(tpMonkeyFunc, funcName, funcName, mockVar, mockType, funcResults, mockVar, parseFunc.Name, mockType, funcParams, mockValue, mockKey) - } - } - for path, content := range pkg { - var ( - buffer bytes.Buffer - dir = strings.Split(path, ".") - mockDir = dir[0] + "/mock" - filename = mockDir + "/monkey_" + dir[1] + ".go" - ) - if _, err = os.Stat(mockDir); os.IsNotExist(err) { - err = nil - os.Mkdir(mockDir, 0744) - } - if _, err := os.Stat(filename); os.IsExist(err) { - continue - } - buffer.WriteString(fmt.Sprintf(tpPackage, "mock")) - buffer.WriteString(content) - content, _ := GoImport(filename, buffer.Bytes()) - ioutil.WriteFile(filename, content, 0644) - } - return -} diff --git a/tool/testgen/main.go b/tool/testgen/main.go deleted file mode 100644 index 4d94e6aec..000000000 --- a/tool/testgen/main.go +++ /dev/null @@ -1,57 +0,0 @@ -package main - -import ( - "flag" - "fmt" - "os" -) - -var ( - err error - _mode, _func string - files []string - parses []*parse -) - -func main() { - flag.StringVar(&_mode, "m", "test", "Generating code by Working mode. [test|interface|mock...]") - flag.StringVar(&_func, "func", "", "Generating code by function.") - flag.Parse() - if len(os.Args) == 1 { - println("Creater is a tool for generating code.\n\nUsage: creater [-m]") - flag.PrintDefaults() - return - } - if err = parseArgs(os.Args[1:], &files, 0); err != nil { - panic(err) - } - switch _mode { - case "monkey": - if parses, err = parseFile(files...); err != nil { - panic(err) - } - if err = genMonkey(parses); err != nil { - panic(err) - } - case "test": - if parses, err = parseFile(files...); err != nil { - panic(err) - } - if err = genTest(parses); err != nil { - panic(err) - } - case "interface": - if parses, err = parseFile(files...); err != nil { - panic(err) - } - if err = genInterface(parses); err != nil { - panic(err) - } - case "mock": - if err = genMock(files...); err != nil { - panic(err) - } - default: - } - fmt.Println(print) -} diff --git a/tool/testgen/parser.go b/tool/testgen/parser.go deleted file mode 100644 index ec363fd04..000000000 --- a/tool/testgen/parser.go +++ /dev/null @@ -1,193 +0,0 @@ -package main - -import ( - "fmt" - "go/ast" - "go/parser" - "go/token" - "io/ioutil" - "os" - "path/filepath" - "strings" -) - -type param struct{ K, V, P string } - -type parse struct { - Path string - Package string - // Imports []string - Imports map[string]*param - // Structs []*param - // Interfaces []string - Funcs []*struct { - Name string - Method, Params, Result []*param - } -} - -func parseArgs(args []string, res *[]string, index int) (err error) { - if len(args) <= index { - return - } - if strings.HasPrefix(args[index], "-") { - index += 2 - parseArgs(args, res, index) - return - } - var f os.FileInfo - if f, err = os.Stat(args[index]); err != nil { - return - } - if f.IsDir() { - if !strings.HasSuffix(args[index], "/") { - args[index] += "/" - } - var fs []os.FileInfo - if fs, err = ioutil.ReadDir(args[index]); err != nil { - return - } - for _, f = range fs { - path, _ := filepath.Abs(args[index] + f.Name()) - args = append(args, path) - } - } else { - if strings.HasSuffix(args[index], ".go") && - !strings.HasSuffix(args[index], "_test.go") { - *res = append(*res, args[index]) - } - } - index++ - return parseArgs(args, res, index) -} - -func parseFile(files ...string) (parses []*parse, err error) { - for _, file := range files { - var ( - astFile *ast.File - fSet = token.NewFileSet() - parse = &parse{ - Imports: make(map[string]*param), - } - ) - if astFile, err = parser.ParseFile(fSet, file, nil, 0); err != nil { - return - } - if astFile.Name != nil { - parse.Path = file - parse.Package = astFile.Name.Name - } - for _, decl := range astFile.Decls { - switch decl.(type) { - case *ast.GenDecl: - if specs := decl.(*ast.GenDecl).Specs; len(specs) > 0 { - parse.Imports = parseImports(specs) - } - case *ast.FuncDecl: - var ( - dec = decl.(*ast.FuncDecl) - parseFunc = &struct { - Name string - Method, Params, Result []*param - }{Name: dec.Name.Name} - ) - if dec.Recv != nil { - parseFunc.Method = parserParams(dec.Recv.List) - } - if dec.Type.Params != nil { - parseFunc.Params = parserParams(dec.Type.Params.List) - } - if dec.Type.Results != nil { - parseFunc.Result = parserParams(dec.Type.Results.List) - } - parse.Funcs = append(parse.Funcs, parseFunc) - } - } - parses = append(parses, parse) - } - return -} - -func parserParams(fields []*ast.Field) (params []*param) { - for _, field := range fields { - p := ¶m{} - p.V = parseType(field.Type) - if field.Names == nil { - params = append(params, p) - } - for _, name := range field.Names { - sp := ¶m{} - sp.K = name.Name - sp.V = p.V - sp.P = p.P - params = append(params, sp) - } - } - return -} - -func parseType(expr ast.Expr) string { - switch expr.(type) { - case *ast.Ident: - return expr.(*ast.Ident).Name - case *ast.StarExpr: - return "*" + parseType(expr.(*ast.StarExpr).X) - case *ast.ArrayType: - return "[" + parseType(expr.(*ast.ArrayType).Len) + "]" + parseType(expr.(*ast.ArrayType).Elt) - case *ast.SelectorExpr: - return parseType(expr.(*ast.SelectorExpr).X) + "." + expr.(*ast.SelectorExpr).Sel.Name - case *ast.MapType: - return "map[" + parseType(expr.(*ast.MapType).Key) + "]" + parseType(expr.(*ast.MapType).Value) - case *ast.StructType: - return "struct{}" - case *ast.InterfaceType: - return "interface{}" - case *ast.FuncType: - var ( - pTemp string - rTemp string - ) - pTemp = parseFuncType(pTemp, expr.(*ast.FuncType).Params) - if expr.(*ast.FuncType).Results != nil { - rTemp = parseFuncType(rTemp, expr.(*ast.FuncType).Results) - return fmt.Sprintf("func(%s) (%s)", pTemp, rTemp) - } - return fmt.Sprintf("func(%s)", pTemp) - case *ast.ChanType: - return fmt.Sprintf("make(chan %s)", parseType(expr.(*ast.ChanType).Value)) - case *ast.Ellipsis: - return parseType(expr.(*ast.Ellipsis).Elt) - } - return "" -} - -func parseFuncType(temp string, data *ast.FieldList) string { - var params = parserParams(data.List) - for i, param := range params { - if i == 0 { - temp = param.K + " " + param.V - continue - } - t := param.K + " " + param.V - temp = fmt.Sprintf("%s, %s", temp, t) - } - return temp -} - -func parseImports(specs []ast.Spec) (params map[string]*param) { - params = make(map[string]*param) - for _, spec := range specs { - switch spec.(type) { - case *ast.ImportSpec: - p := ¶m{V: strings.Replace(spec.(*ast.ImportSpec).Path.Value, "\"", "", -1)} - if spec.(*ast.ImportSpec).Name != nil { - p.K = spec.(*ast.ImportSpec).Name.Name - params[p.K] = p - } else { - vs := strings.Split(p.V, "/") - params[vs[len(vs)-1]] = p - } - } - } - return -} diff --git a/tool/testgen/templete.go b/tool/testgen/templete.go deleted file mode 100644 index bb42d5831..000000000 --- a/tool/testgen/templete.go +++ /dev/null @@ -1,41 +0,0 @@ -package main - -var ( - tpPackage = "package %s\n\n" - tpImport = "import (\n\t%s\n)\n\n" - tpVar = "var (\n\t%s\n)\n" - tpInterface = "type %sInterface interface {\n%s}\n" - tpIntfcFunc = "%s(%s) %s\n" - tpMonkeyFunc = "// Mock%s .\nfunc Mock%s(%s %s,%s) (guard *monkey.PatchGuard) {\n\treturn monkey.PatchInstanceMethod(reflect.TypeOf(%s), \"%s\", func(_ %s, %s) (%s) {\n\t\treturn %s\n\t})\n}\n\n" - tpTestReset = "\n\t\tReset(func() {%s\n\t\t})" - tpTestFunc = "func Test%s%s(t *testing.T){%s\n\tConvey(\"%s\", t, func(){\n\t\t%s\tConvey(\"When everything goes positive\", func(){\n\t\t\t%s\n\t\t\t})\n\t\t})%s\n\t})\n}\n\n" - tpTestDaoMain = `func TestMain(m *testing.M) { - flag.Set("conf", "%s") - flag.Parse() - %s - os.Exit(m.Run()) -} -` - tpTestServiceMain = `func TestMain(m *testing.M){ - flag.Set("conf", "%s") - flag.Parse() - %s - os.Exit(m.Run()) -} -` - tpTestMainNew = `if err := paladin.Init(); err != nil { - panic(err) - } - %s` - tpTestMainOld = `if err := conf.Init(); err != nil { - panic(err) - } - %s` - print = `Generation success! - 莫生气 - 代码辣鸡非我意, - 自己动手分田地; - 你若气死谁如意? - 谈笑风生活长命. -// Release 1.2.3. Powered by Kratos` -) diff --git a/tool/testgen/utils.go b/tool/testgen/utils.go deleted file mode 100644 index 025fb91a7..000000000 --- a/tool/testgen/utils.go +++ /dev/null @@ -1,42 +0,0 @@ -package main - -import ( - "fmt" - "strings" - - "golang.org/x/tools/imports" -) - -// GoImport Use golang.org/x/tools/imports auto import pkg -func GoImport(file string, bytes []byte) (res []byte, err error) { - options := &imports.Options{ - TabWidth: 8, - TabIndent: true, - Comments: true, - Fragment: true, - } - if res, err = imports.Process(file, bytes, options); err != nil { - fmt.Printf("GoImport(%s) error(%v)", file, err) - res = bytes - return - } - return -} - -// ConvertMethod checkout the file belongs to dao or not -func ConvertMethod(path string) (method string) { - switch { - case strings.Contains(path, "/dao"): - method = "d" - case strings.Contains(path, "/service"): - method = "s" - default: - method = "" - } - return -} - -//ConvertHump convert words to hump style -func ConvertHump(words string) string { - return strings.ToUpper(words[0:1]) + words[1:] -} diff --git a/transport/grpc/client.go b/transport/grpc/client.go new file mode 100644 index 000000000..c89ec488d --- /dev/null +++ b/transport/grpc/client.go @@ -0,0 +1,115 @@ +package grpc + +import ( + "context" + "time" + + "github.com/go-kratos/kratos/v2/middleware" + "github.com/go-kratos/kratos/v2/middleware/recovery" + "github.com/go-kratos/kratos/v2/middleware/status" + "github.com/go-kratos/kratos/v2/registry" + "github.com/go-kratos/kratos/v2/transport" + "github.com/go-kratos/kratos/v2/transport/grpc/resolver/discovery" + + "google.golang.org/grpc" +) + +// ClientOption is gRPC client option. +type ClientOption func(o *clientOptions) + +// WithEndpoint with client endpoint. +func WithEndpoint(endpoint string) ClientOption { + return func(o *clientOptions) { + o.endpoint = endpoint + } +} + +// WithTimeout with client timeout. +func WithTimeout(timeout time.Duration) ClientOption { + return func(o *clientOptions) { + o.timeout = timeout + } +} + +// WithMiddleware with client middleware. +func WithMiddleware(m middleware.Middleware) ClientOption { + return func(o *clientOptions) { + o.middleware = m + } +} + +// WithRegistry with client registry. +func WithRegistry(r registry.Registry) ClientOption { + return func(o *clientOptions) { + o.registry = r + } +} + +// WithOptions with gRPC options. +func WithOptions(opts ...grpc.DialOption) ClientOption { + return func(o *clientOptions) { + o.grpcOpts = opts + } +} + +// clientOptions is gRPC Client +type clientOptions struct { + endpoint string + timeout time.Duration + middleware middleware.Middleware + registry registry.Registry + grpcOpts []grpc.DialOption +} + +// Dial returns a GRPC connection. +func Dial(ctx context.Context, opts ...ClientOption) (*grpc.ClientConn, error) { + return dial(ctx, false, opts...) +} + +// DialInsecure returns an insecure GRPC connection. +func DialInsecure(ctx context.Context, opts ...ClientOption) (*grpc.ClientConn, error) { + return dial(ctx, true, opts...) +} + +func dial(ctx context.Context, insecure bool, opts ...ClientOption) (*grpc.ClientConn, error) { + options := clientOptions{ + timeout: 500 * time.Millisecond, + middleware: middleware.Chain( + recovery.Recovery(), + status.Client(), + ), + } + for _, o := range opts { + o(&options) + } + var grpcOpts = []grpc.DialOption{ + grpc.WithTimeout(options.timeout), + grpc.WithUnaryInterceptor(UnaryClientInterceptor(options.middleware)), + } + if options.registry != nil { + grpc.WithResolvers(discovery.NewBuilder(options.registry)) + } + if insecure { + grpcOpts = append(grpcOpts, grpc.WithInsecure()) + } + if len(options.grpcOpts) > 0 { + grpcOpts = append(grpcOpts, options.grpcOpts...) + } + return grpc.DialContext(ctx, options.endpoint, grpcOpts...) +} + +// UnaryClientInterceptor retruns a unary client interceptor. +func UnaryClientInterceptor(m middleware.Middleware) grpc.UnaryClientInterceptor { + return func(ctx context.Context, method string, req, reply interface{}, cc *grpc.ClientConn, invoker grpc.UnaryInvoker, opts ...grpc.CallOption) error { + ctx = transport.NewContext(ctx, transport.Transport{Kind: "gRPC"}) + ctx = NewClientContext(ctx, ClientInfo{FullMethod: method}) + h := func(ctx context.Context, req interface{}) (interface{}, error) { + return reply, invoker(ctx, method, req, reply, cc, opts...) + } + if m != nil { + h = m(h) + } + _, err := h(ctx, req) + return err + } +} diff --git a/transport/grpc/context.go b/transport/grpc/context.go new file mode 100644 index 000000000..b9dc83790 --- /dev/null +++ b/transport/grpc/context.go @@ -0,0 +1,43 @@ +package grpc + +import "context" + +// ServerInfo is gRPC server infomation. +type ServerInfo struct { + // Server is the service implementation the user provides. This is read-only. + Server interface{} + // FullMethod is the full RPC method string, i.e., /package.service/method. + FullMethod string +} + +type serverKey struct{} + +// NewServerContext returns a new Context that carries value. +func NewServerContext(ctx context.Context, info ServerInfo) context.Context { + return context.WithValue(ctx, serverKey{}, info) +} + +// FromServerContext returns the Transport value stored in ctx, if any. +func FromServerContext(ctx context.Context) (info ServerInfo, ok bool) { + info, ok = ctx.Value(serverKey{}).(ServerInfo) + return +} + +// ClientInfo is gRPC server infomation. +type ClientInfo struct { + // FullMethod is the full RPC method string, i.e., /package.service/method. + FullMethod string +} + +type clientKey struct{} + +// NewClientContext returns a new Context that carries value. +func NewClientContext(ctx context.Context, info ClientInfo) context.Context { + return context.WithValue(ctx, serverKey{}, info) +} + +// FromClientContext returns the Transport value stored in ctx, if any. +func FromClientContext(ctx context.Context) (info ClientInfo, ok bool) { + info, ok = ctx.Value(serverKey{}).(ClientInfo) + return +} diff --git a/transport/grpc/resolver/direct/builder.go b/transport/grpc/resolver/direct/builder.go new file mode 100644 index 000000000..fefbe7474 --- /dev/null +++ b/transport/grpc/resolver/direct/builder.go @@ -0,0 +1,35 @@ +package direct + +import ( + "strings" + + "google.golang.org/grpc/resolver" +) + +func init() { + resolver.Register(NewBuilder()) +} + +type directBuilder struct{} + +// NewBuilder creates a directBuilder which is used to factory direct resolvers. +// example: +// direct:///127.0.0.1:9000,127.0.0.2:9000 +func NewBuilder() resolver.Builder { + return &directBuilder{} +} + +func (d *directBuilder) Build(target resolver.Target, cc resolver.ClientConn, opts resolver.BuildOptions) (resolver.Resolver, error) { + var addrs []resolver.Address + for _, addr := range strings.Split(target.Endpoint, ",") { + addrs = append(addrs, resolver.Address{Addr: addr}) + } + cc.UpdateState(resolver.State{ + Addresses: addrs, + }) + return newDirectResolver(), nil +} + +func (d *directBuilder) Scheme() string { + return "direct" +} diff --git a/transport/grpc/resolver/direct/resolver.go b/transport/grpc/resolver/direct/resolver.go new file mode 100644 index 000000000..7d18228c8 --- /dev/null +++ b/transport/grpc/resolver/direct/resolver.go @@ -0,0 +1,15 @@ +package direct + +import "google.golang.org/grpc/resolver" + +type directResolver struct{} + +func newDirectResolver() resolver.Resolver { + return &directResolver{} +} + +func (r *directResolver) Close() { +} + +func (r *directResolver) ResolveNow(options resolver.ResolveNowOptions) { +} diff --git a/transport/grpc/resolver/discovery/builder.go b/transport/grpc/resolver/discovery/builder.go new file mode 100644 index 000000000..f5fa279ab --- /dev/null +++ b/transport/grpc/resolver/discovery/builder.go @@ -0,0 +1,54 @@ +package discovery + +import ( + "github.com/go-kratos/kratos/v2/log" + "github.com/go-kratos/kratos/v2/registry" + "google.golang.org/grpc/resolver" +) + +const name = "discovery" + +// Option is builder option. +type Option func(o *builder) + +// WithLogger with builder logger. +func WithLogger(logger log.Logger) Option { + return func(o *builder) { + o.logger = logger + } +} + +type builder struct { + registry registry.Registry + logger log.Logger +} + +// NewBuilder creates a builder which is used to factory registry resolvers. +func NewBuilder(r registry.Registry, opts ...Option) resolver.Builder { + b := &builder{ + registry: r, + logger: log.DefaultLogger, + } + for _, o := range opts { + o(b) + } + return b +} + +func (d *builder) Build(target resolver.Target, cc resolver.ClientConn, opts resolver.BuildOptions) (resolver.Resolver, error) { + w, err := d.registry.Watch(target.Endpoint) + if err != nil { + return nil, err + } + r := &discoveryResolver{ + w: w, + cc: cc, + log: log.NewHelper("grpc/resolver/discovery", d.logger), + } + go r.watch() + return r, nil +} + +func (d *builder) Scheme() string { + return name +} diff --git a/transport/grpc/resolver/discovery/resolver.go b/transport/grpc/resolver/discovery/resolver.go new file mode 100644 index 000000000..a25f127aa --- /dev/null +++ b/transport/grpc/resolver/discovery/resolver.go @@ -0,0 +1,74 @@ +package discovery + +import ( + "net/url" + "time" + + "github.com/go-kratos/kratos/v2/log" + "github.com/go-kratos/kratos/v2/registry" + "google.golang.org/grpc/attributes" + "google.golang.org/grpc/resolver" +) + +type discoveryResolver struct { + w registry.Watcher + cc resolver.ClientConn + log *log.Helper +} + +func (r *discoveryResolver) watch() { + for { + ins, err := r.w.Next() + if err != nil { + r.log.Errorf("Failed to watch discovery endpoint: %v", err) + time.Sleep(time.Second) + continue + } + r.update(ins) + } +} + +func (r *discoveryResolver) update(ins []*registry.ServiceInstance) { + var addrs []resolver.Address + for _, in := range ins { + endpoint, err := parseEndpoint(in.Endpoints) + if err != nil { + r.log.Errorf("Failed to parse discovery endpoint: %v", err) + continue + } + addr := resolver.Address{ + ServerName: in.Name, + Attributes: parseAttributes(in.Metadata), + Addr: endpoint, + } + addrs = append(addrs, addr) + } + r.cc.UpdateState(resolver.State{Addresses: addrs}) +} + +func (r *discoveryResolver) Close() { + r.w.Close() +} + +func (r *discoveryResolver) ResolveNow(options resolver.ResolveNowOptions) {} + +func parseEndpoint(endpoints []string) (string, error) { + for _, e := range endpoints { + u, err := url.Parse(e) + if err != nil { + return "", err + } + if u.Scheme == "grpc" { + return u.Host, nil + } + } + return "", nil +} + +func parseAttributes(md map[string]string) *attributes.Attributes { + var pairs []interface{} + for k, v := range md { + pairs = append(pairs, k, v) + } + return attributes.New(pairs) +} diff --git a/transport/grpc/server.go b/transport/grpc/server.go new file mode 100644 index 000000000..ce118660a --- /dev/null +++ b/transport/grpc/server.go @@ -0,0 +1,159 @@ +package grpc + +import ( + "context" + "fmt" + "net" + "time" + + "github.com/go-kratos/kratos/v2/internal/host" + "github.com/go-kratos/kratos/v2/log" + "github.com/go-kratos/kratos/v2/middleware" + "github.com/go-kratos/kratos/v2/middleware/recovery" + "github.com/go-kratos/kratos/v2/middleware/status" + "github.com/go-kratos/kratos/v2/transport" + + "google.golang.org/grpc" +) + +const loggerName = "transport/grpc" + +var _ transport.Server = (*Server)(nil) + +// ServerOption is gRPC server option. +type ServerOption func(o *Server) + +// Network with server network. +func Network(network string) ServerOption { + return func(s *Server) { + s.network = network + } +} + +// Address with server address. +func Address(addr string) ServerOption { + return func(s *Server) { + s.address = addr + } +} + +// Timeout with server timeout. +func Timeout(timeout time.Duration) ServerOption { + return func(s *Server) { + s.timeout = timeout + } +} + +// Logger with server logger. +func Logger(logger log.Logger) ServerOption { + return func(s *Server) { + s.log = log.NewHelper(loggerName, logger) + } +} + +// Middleware with server middleware. +func Middleware(m middleware.Middleware) ServerOption { + return func(s *Server) { + s.middleware = m + } +} + +// Options with grpc options. +func Options(opts ...grpc.ServerOption) ServerOption { + return func(s *Server) { + s.grpcOpts = opts + } +} + +// Server is a gRPC server wrapper. +type Server struct { + *grpc.Server + lis net.Listener + network string + address string + timeout time.Duration + log *log.Helper + middleware middleware.Middleware + grpcOpts []grpc.ServerOption +} + +// NewServer creates a gRPC server by options. +func NewServer(opts ...ServerOption) *Server { + srv := &Server{ + network: "tcp", + address: ":0", + timeout: time.Second, + log: log.NewHelper(loggerName, log.DefaultLogger), + middleware: middleware.Chain( + recovery.Recovery(), + status.Server(), + ), + } + for _, o := range opts { + o(srv) + } + var grpcOpts = []grpc.ServerOption{ + grpc.ChainUnaryInterceptor( + UnaryServerInterceptor(srv.middleware), + UnaryTimeoutInterceptor(srv.timeout), + ), + } + if len(srv.grpcOpts) > 0 { + grpcOpts = append(grpcOpts, srv.grpcOpts...) + } + srv.Server = grpc.NewServer(grpcOpts...) + return srv +} + +// Endpoint return a real address to registry endpoint. +// examples: +// grpc://127.0.0.1:9000?isSecure=false +func (s *Server) Endpoint() (string, error) { + addr, err := host.Extract(s.address, s.lis) + if err != nil { + return "", err + } + return fmt.Sprintf("grpc://%s", addr), nil +} + +// Start start the gRPC server. +func (s *Server) Start() error { + lis, err := net.Listen(s.network, s.address) + if err != nil { + return err + } + s.lis = lis + s.log.Infof("[gRPC] server listening on: %s", lis.Addr().String()) + return s.Serve(lis) +} + +// Stop stop the gRPC server. +func (s *Server) Stop() error { + s.GracefulStop() + s.log.Info("[gRPC] server stopping") + return nil +} + +// UnaryTimeoutInterceptor returns a unary timeout interceptor. +func UnaryTimeoutInterceptor(timeout time.Duration) grpc.UnaryServerInterceptor { + return func(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (interface{}, error) { + ctx, cancel := context.WithTimeout(ctx, timeout) + defer cancel() + return handler(ctx, req) + } +} + +// UnaryServerInterceptor returns a unary server interceptor. +func UnaryServerInterceptor(m middleware.Middleware) grpc.UnaryServerInterceptor { + return func(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (interface{}, error) { + ctx = transport.NewContext(ctx, transport.Transport{Kind: "gRPC"}) + ctx = NewServerContext(ctx, ServerInfo{Server: info.Server, FullMethod: info.FullMethod}) + h := func(ctx context.Context, req interface{}) (interface{}, error) { + return handler(ctx, req) + } + if m != nil { + h = m(h) + } + return h(ctx, req) + } +} diff --git a/transport/grpc/server_test.go b/transport/grpc/server_test.go new file mode 100644 index 000000000..23b914f82 --- /dev/null +++ b/transport/grpc/server_test.go @@ -0,0 +1,40 @@ +package grpc + +import ( + "context" + "fmt" + "testing" + "time" + + "github.com/go-kratos/kratos/v2/internal/host" +) + +func TestServer(t *testing.T) { + srv := NewServer() + if endpoint, err := srv.Endpoint(); err != nil || endpoint == "" { + t.Fatal(endpoint, err) + } + + time.AfterFunc(time.Second, func() { + defer srv.Stop() + testClient(t, srv) + }) + // start server + if err := srv.Start(); err != nil { + t.Fatal(err) + } +} + +func testClient(t *testing.T, srv *Server) { + port, ok := host.Port(srv.lis) + if !ok { + t.Fatalf("extract port error: %v", srv.lis) + } + endpoint := fmt.Sprintf("127.0.0.1:%d", port) + // new a gRPC client + conn, err := DialInsecure(context.Background(), WithEndpoint(endpoint)) + if err != nil { + t.Fatal(err) + } + conn.Close() +} diff --git a/transport/http/bind.go b/transport/http/bind.go new file mode 100644 index 000000000..d0a1d7fed --- /dev/null +++ b/transport/http/bind.go @@ -0,0 +1,287 @@ +package http + +import ( + "encoding/base64" + "errors" + "fmt" + "log" + "net/http" + "strconv" + "strings" + "time" + + "github.com/golang/protobuf/ptypes" + "github.com/golang/protobuf/ptypes/wrappers" + "google.golang.org/genproto/protobuf/field_mask" + "google.golang.org/protobuf/proto" + "google.golang.org/protobuf/reflect/protoreflect" + "google.golang.org/protobuf/reflect/protoregistry" +) + +// BindVars parses url parameters. +func BindVars(req *http.Request, msg proto.Message) error { + for key, value := range Vars(req) { + if err := populateFieldValues(msg.ProtoReflect(), strings.Split(key, "."), []string{value}); err != nil { + return err + } + } + return nil +} + +// BindForm parses form parameters. +func BindForm(req *http.Request, msg proto.Message) error { + if err := req.ParseForm(); err != nil { + return err + } + for key, values := range req.Form { + if err := populateFieldValues(msg.ProtoReflect(), strings.Split(key, "."), values); err != nil { + return err + } + } + return nil +} + +func populateFieldValues(v protoreflect.Message, fieldPath []string, values []string) error { + if len(fieldPath) < 1 { + return errors.New("no field path") + } + if len(values) < 1 { + return errors.New("no value provided") + } + var fd protoreflect.FieldDescriptor + for i, fieldName := range fieldPath { + fields := v.Descriptor().Fields() + + if fd = fields.ByName(protoreflect.Name(fieldName)); fd == nil { + fd = fields.ByJSONName(fieldName) + if fd == nil { + log.Printf("field not found in %q: %q\n", v.Descriptor().FullName(), strings.Join(fieldPath, ".")) + return nil + } + } + + if i == len(fieldPath)-1 { + break + } + + if fd.Message() == nil || fd.Cardinality() == protoreflect.Repeated { + return fmt.Errorf("invalid path: %q is not a message", fieldName) + } + + v = v.Mutable(fd).Message() + } + if of := fd.ContainingOneof(); of != nil { + if f := v.WhichOneof(of); f != nil { + return fmt.Errorf("field already set for oneof %q", of.FullName().Name()) + } + } + switch { + case fd.IsList(): + return populateRepeatedField(fd, v.Mutable(fd).List(), values) + case fd.IsMap(): + return populateMapField(fd, v.Mutable(fd).Map(), values) + } + if len(values) > 1 { + return fmt.Errorf("too many values for field %q: %s", fd.FullName().Name(), strings.Join(values, ", ")) + } + return populateField(fd, v, values[0]) +} + +func populateField(fd protoreflect.FieldDescriptor, v protoreflect.Message, value string) error { + val, err := parseField(fd, value) + if err != nil { + return fmt.Errorf("parsing field %q: %w", fd.FullName().Name(), err) + } + v.Set(fd, val) + return nil +} + +func populateRepeatedField(fd protoreflect.FieldDescriptor, list protoreflect.List, values []string) error { + for _, value := range values { + v, err := parseField(fd, value) + if err != nil { + return fmt.Errorf("parsing list %q: %w", fd.FullName().Name(), err) + } + list.Append(v) + } + return nil +} + +func populateMapField(fd protoreflect.FieldDescriptor, mp protoreflect.Map, values []string) error { + if len(values) != 2 { + return fmt.Errorf("more than one value provided for key %q in map %q", values[0], fd.FullName()) + } + key, err := parseField(fd.MapKey(), values[0]) + if err != nil { + return fmt.Errorf("parsing map key %q: %w", fd.FullName().Name(), err) + } + value, err := parseField(fd.MapValue(), values[1]) + if err != nil { + return fmt.Errorf("parsing map value %q: %w", fd.FullName().Name(), err) + } + mp.Set(key.MapKey(), value) + return nil +} + +func parseField(fd protoreflect.FieldDescriptor, value string) (protoreflect.Value, error) { + switch fd.Kind() { + case protoreflect.BoolKind: + v, err := strconv.ParseBool(value) + if err != nil { + return protoreflect.Value{}, err + } + return protoreflect.ValueOfBool(v), nil + case protoreflect.EnumKind: + enum, err := protoregistry.GlobalTypes.FindEnumByName(fd.Enum().FullName()) + switch { + case errors.Is(err, protoregistry.NotFound): + return protoreflect.Value{}, fmt.Errorf("enum %q is not registered", fd.Enum().FullName()) + case err != nil: + return protoreflect.Value{}, fmt.Errorf("failed to look up enum: %w", err) + } + v := enum.Descriptor().Values().ByName(protoreflect.Name(value)) + if v == nil { + i, err := strconv.Atoi(value) + if err != nil { + return protoreflect.Value{}, fmt.Errorf("%q is not a valid value", value) + } + v = enum.Descriptor().Values().ByNumber(protoreflect.EnumNumber(i)) + if v == nil { + return protoreflect.Value{}, fmt.Errorf("%q is not a valid value", value) + } + } + return protoreflect.ValueOfEnum(v.Number()), nil + case protoreflect.Int32Kind, protoreflect.Sint32Kind, protoreflect.Sfixed32Kind: + v, err := strconv.ParseInt(value, 10, 32) + if err != nil { + return protoreflect.Value{}, err + } + return protoreflect.ValueOfInt32(int32(v)), nil + case protoreflect.Int64Kind, protoreflect.Sint64Kind, protoreflect.Sfixed64Kind: + v, err := strconv.ParseInt(value, 10, 64) + if err != nil { + return protoreflect.Value{}, err + } + return protoreflect.ValueOfInt64(v), nil + case protoreflect.Uint32Kind, protoreflect.Fixed32Kind: + v, err := strconv.ParseUint(value, 10, 32) + if err != nil { + return protoreflect.Value{}, err + } + return protoreflect.ValueOfUint32(uint32(v)), nil + case protoreflect.Uint64Kind, protoreflect.Fixed64Kind: + v, err := strconv.ParseUint(value, 10, 64) + if err != nil { + return protoreflect.Value{}, err + } + return protoreflect.ValueOfUint64(v), nil + case protoreflect.FloatKind: + v, err := strconv.ParseFloat(value, 32) + if err != nil { + return protoreflect.Value{}, err + } + return protoreflect.ValueOfFloat32(float32(v)), nil + case protoreflect.DoubleKind: + v, err := strconv.ParseFloat(value, 64) + if err != nil { + return protoreflect.Value{}, err + } + return protoreflect.ValueOfFloat64(v), nil + case protoreflect.StringKind: + return protoreflect.ValueOfString(value), nil + case protoreflect.BytesKind: + v, err := base64.StdEncoding.DecodeString(value) + if err != nil { + return protoreflect.Value{}, err + } + return protoreflect.ValueOfBytes(v), nil + case protoreflect.MessageKind, protoreflect.GroupKind: + return parseMessage(fd.Message(), value) + default: + panic(fmt.Sprintf("unknown field kind: %v", fd.Kind())) + } +} + +func parseMessage(md protoreflect.MessageDescriptor, value string) (protoreflect.Value, error) { + var msg proto.Message + switch md.FullName() { + case "google.protobuf.Timestamp": + if value == "null" { + break + } + t, err := time.Parse(time.RFC3339Nano, value) + if err != nil { + return protoreflect.Value{}, err + } + msg, err = ptypes.TimestampProto(t) + if err != nil { + return protoreflect.Value{}, err + } + case "google.protobuf.Duration": + if value == "null" { + break + } + d, err := time.ParseDuration(value) + if err != nil { + return protoreflect.Value{}, err + } + msg = ptypes.DurationProto(d) + case "google.protobuf.DoubleValue": + v, err := strconv.ParseFloat(value, 64) + if err != nil { + return protoreflect.Value{}, err + } + msg = &wrappers.DoubleValue{Value: v} + case "google.protobuf.FloatValue": + v, err := strconv.ParseFloat(value, 32) + if err != nil { + return protoreflect.Value{}, err + } + msg = &wrappers.FloatValue{Value: float32(v)} + case "google.protobuf.Int64Value": + v, err := strconv.ParseInt(value, 10, 64) + if err != nil { + return protoreflect.Value{}, err + } + msg = &wrappers.Int64Value{Value: v} + case "google.protobuf.Int32Value": + v, err := strconv.ParseInt(value, 10, 32) + if err != nil { + return protoreflect.Value{}, err + } + msg = &wrappers.Int32Value{Value: int32(v)} + case "google.protobuf.UInt64Value": + v, err := strconv.ParseUint(value, 10, 64) + if err != nil { + return protoreflect.Value{}, err + } + msg = &wrappers.UInt64Value{Value: v} + case "google.protobuf.UInt32Value": + v, err := strconv.ParseUint(value, 10, 32) + if err != nil { + return protoreflect.Value{}, err + } + msg = &wrappers.UInt32Value{Value: uint32(v)} + case "google.protobuf.BoolValue": + v, err := strconv.ParseBool(value) + if err != nil { + return protoreflect.Value{}, err + } + msg = &wrappers.BoolValue{Value: v} + case "google.protobuf.StringValue": + msg = &wrappers.StringValue{Value: value} + case "google.protobuf.BytesValue": + v, err := base64.StdEncoding.DecodeString(value) + if err != nil { + return protoreflect.Value{}, err + } + msg = &wrappers.BytesValue{Value: v} + case "google.protobuf.FieldMask": + fm := &field_mask.FieldMask{} + fm.Paths = append(fm.Paths, strings.Split(value, ",")...) + msg = fm + default: + return protoreflect.Value{}, fmt.Errorf("unsupported message type: %q", string(md.FullName())) + } + return protoreflect.ValueOfMessage(msg.ProtoReflect()), nil +} diff --git a/transport/http/client.go b/transport/http/client.go new file mode 100644 index 000000000..358bd3b0b --- /dev/null +++ b/transport/http/client.go @@ -0,0 +1,136 @@ +package http + +import ( + "context" + "io/ioutil" + "net/http" + "time" + + "github.com/go-kratos/kratos/v2/encoding" + "github.com/go-kratos/kratos/v2/errors" + "github.com/go-kratos/kratos/v2/middleware" + "github.com/go-kratos/kratos/v2/transport" +) + +// ClientOption is HTTP client option. +type ClientOption func(*clientOptions) + +// WithTimeout with client request timeout. +func WithTimeout(d time.Duration) ClientOption { + return func(o *clientOptions) { + o.timeout = d + } +} + +// WithUserAgent with client user agent. +func WithUserAgent(ua string) ClientOption { + return func(o *clientOptions) { + o.userAgent = ua + } +} + +// WithTransport with client transport. +func WithTransport(trans http.RoundTripper) ClientOption { + return func(o *clientOptions) { + o.transport = trans + } +} + +// WithMiddleware with client middleware. +func WithMiddleware(m middleware.Middleware) ClientOption { + return func(o *clientOptions) { + o.middleware = m + } +} + +// Client is a HTTP transport client. +type clientOptions struct { + ctx context.Context + timeout time.Duration + userAgent string + transport http.RoundTripper + middleware middleware.Middleware +} + +// NewClient returns an HTTP client. +func NewClient(ctx context.Context, opts ...ClientOption) (*http.Client, error) { + trans, err := NewTransport(ctx, opts...) + if err != nil { + return nil, err + } + return &http.Client{Transport: trans}, nil +} + +// NewTransport creates an http.RoundTripper. +func NewTransport(ctx context.Context, opts ...ClientOption) (http.RoundTripper, error) { + options := &clientOptions{ + ctx: ctx, + timeout: 500 * time.Millisecond, + transport: http.DefaultTransport, + } + for _, o := range opts { + o(options) + } + return &baseTransport{ + middleware: options.middleware, + userAgent: options.userAgent, + timeout: options.timeout, + base: options.transport, + }, nil +} + +type baseTransport struct { + userAgent string + timeout time.Duration + base http.RoundTripper + middleware middleware.Middleware +} + +func (t *baseTransport) RoundTrip(req *http.Request) (*http.Response, error) { + if t.userAgent != "" && req.Header.Get("User-Agent") == "" { + req.Header.Set("User-Agent", t.userAgent) + } + ctx := transport.NewContext(req.Context(), transport.Transport{Kind: "HTTP"}) + ctx = NewClientContext(ctx, ClientInfo{Request: req}) + ctx, cancel := context.WithTimeout(ctx, t.timeout) + defer cancel() + + h := func(ctx context.Context, in interface{}) (interface{}, error) { + return t.base.RoundTrip(req) + } + if t.middleware != nil { + h = t.middleware(h) + } + res, err := h(ctx, req) + if err != nil { + return nil, err + } + return res.(*http.Response), nil +} + +// Do send an HTTP request and decodes the body of response into target. +// returns an error (of type *Error) if the response status code is not 2xx. +func Do(client *http.Client, req *http.Request, target interface{}) error { + res, err := client.Do(req) + if err != nil { + return err + } + data, err := ioutil.ReadAll(res.Body) + if err != nil { + return err + } + defer res.Body.Close() + subtype := contentSubtype(res.Header.Get("content-type")) + codec := encoding.GetCodec(subtype) + if codec == nil { + codec = encoding.GetCodec("json") + } + if res.StatusCode < 200 || res.StatusCode > 299 { + se := &errors.StatusError{} + if err := codec.Unmarshal(data, se); err != nil { + return err + } + return se + } + return codec.Unmarshal(data, target) +} diff --git a/transport/http/context.go b/transport/http/context.go new file mode 100644 index 000000000..d12856e34 --- /dev/null +++ b/transport/http/context.go @@ -0,0 +1,50 @@ +package http + +import ( + "context" + "net/http" + + "github.com/gorilla/mux" +) + +// ServerInfo is HTTP server infomation. +type ServerInfo struct { + Request *http.Request + Response http.ResponseWriter +} + +type serverKey struct{} + +// NewServerContext returns a new Context that carries value. +func NewServerContext(ctx context.Context, info ServerInfo) context.Context { + return context.WithValue(ctx, serverKey{}, info) +} + +// FromServerContext returns the Transport value stored in ctx, if any. +func FromServerContext(ctx context.Context) (info ServerInfo, ok bool) { + info, ok = ctx.Value(serverKey{}).(ServerInfo) + return +} + +// ClientInfo is HTTP client infomation. +type ClientInfo struct { + Request *http.Request +} + +type clientKey struct{} + +// NewClientContext returns a new Context that carries value. +func NewClientContext(ctx context.Context, info ClientInfo) context.Context { + return context.WithValue(ctx, clientKey{}, info) +} + +// FromClientContext returns the Transport value stored in ctx, if any. +func FromClientContext(ctx context.Context) (info ClientInfo, ok bool) { + info, ok = ctx.Value(clientKey{}).(ClientInfo) + return +} + +// Vars returns the route variables for the current request, if any. +func Vars(req *http.Request) map[string]string { + return mux.Vars(req) +} diff --git a/transport/http/default.go b/transport/http/default.go new file mode 100644 index 000000000..61f7e1576 --- /dev/null +++ b/transport/http/default.go @@ -0,0 +1,81 @@ +package http + +import ( + "fmt" + "io/ioutil" + "net/http" + "strings" + + "github.com/go-kratos/kratos/v2/encoding" +) + +const baseContentType = "application" + +func contentType(subtype string) string { + return strings.Join([]string{baseContentType, subtype}, "/") +} + +func contentSubtype(contentType string) string { + if contentType == baseContentType { + return "" + } + if !strings.HasPrefix(contentType, baseContentType) { + return "" + } + // guaranteed since != baseContentType and has baseContentType prefix + switch contentType[len(baseContentType)] { + case '/', ';': + // this will return true for "application/grpc+" or "application/grpc;" + // which the previous validContentType function tested to be valid, so we + // just say that no content-subtype is specified in this case + return contentType[len(baseContentType)+1:] + default: + return "" + } +} + +func defaultRequestDecoder(req *http.Request, v interface{}) error { + data, err := ioutil.ReadAll(req.Body) + if err != nil { + return err + } + defer req.Body.Close() + subtype := contentSubtype(req.Header.Get("content-type")) + codec := encoding.GetCodec(subtype) + if codec == nil { + return fmt.Errorf("decoding request failed unknown content-type: %s", subtype) + } + return codec.Unmarshal(data, v) +} + +func defaultResponseEncoder(res http.ResponseWriter, req *http.Request, v interface{}) error { + subtype := contentSubtype(req.Header.Get("accept")) + codec := encoding.GetCodec(subtype) + if codec == nil { + codec = encoding.GetCodec("json") + } + data, err := codec.Marshal(v) + if err != nil { + return err + } + res.Header().Set("content-type", contentType(codec.Name())) + res.Write(data) + return nil +} + +func defaultErrorEncoder(res http.ResponseWriter, req *http.Request, err error) { + se, code := StatusError(err) + subtype := contentSubtype(req.Header.Get("accept")) + codec := encoding.GetCodec(subtype) + if codec == nil { + codec = encoding.GetCodec("json") + } + data, err := codec.Marshal(se) + if err != nil { + res.WriteHeader(http.StatusInternalServerError) + return + } + res.Header().Set("content-type", contentType(codec.Name())) + res.WriteHeader(code) + res.Write(data) +} diff --git a/transport/http/errors.go b/transport/http/errors.go new file mode 100644 index 000000000..a0c016530 --- /dev/null +++ b/transport/http/errors.go @@ -0,0 +1,59 @@ +package http + +import ( + "net/http" + + "github.com/go-kratos/kratos/v2/errors" +) + +var ( + // References: https://github.com/googleapis/googleapis/blob/master/google/rpc/code.proto + codesMapping = map[int32]int{ + 0: http.StatusOK, + 1: http.StatusInternalServerError, + 2: http.StatusInternalServerError, + 3: http.StatusBadRequest, + 4: http.StatusRequestTimeout, + 5: http.StatusNotFound, + 6: http.StatusConflict, + 7: http.StatusForbidden, + 8: http.StatusTooManyRequests, + 9: http.StatusPreconditionFailed, + 10: http.StatusConflict, + 11: http.StatusBadRequest, + 12: http.StatusNotImplemented, + 13: http.StatusInternalServerError, + 14: http.StatusServiceUnavailable, + 15: http.StatusInternalServerError, + 16: http.StatusUnauthorized, + } + statusMapping = map[int]int32{ + http.StatusOK: 0, + http.StatusBadRequest: 3, + http.StatusRequestTimeout: 4, + http.StatusNotFound: 5, + http.StatusConflict: 6, + http.StatusForbidden: 7, + http.StatusUnauthorized: 16, + http.StatusPreconditionFailed: 9, + http.StatusNotImplemented: 12, + http.StatusInternalServerError: 13, + http.StatusServiceUnavailable: 14, + } +) + +// StatusError converts error to status error. +func StatusError(err error) (*errors.StatusError, int) { + se, ok := errors.FromError(err) + if !ok { + se = &errors.StatusError{ + Code: 2, + Reason: "Unknown", + Message: "Unknown: " + err.Error(), + } + } + if status, ok := codesMapping[se.Code]; ok { + return se, status + } + return se, http.StatusInternalServerError +} diff --git a/transport/http/route.go b/transport/http/route.go new file mode 100644 index 000000000..86bbcfc3f --- /dev/null +++ b/transport/http/route.go @@ -0,0 +1,55 @@ +package http + +import ( + "net/http" + + "github.com/gorilla/mux" +) + +// RouteGroup adds a matcher for the URL path and method. This matches if the given +// template is a prefix of the full URL path. See route.Path() for details on +// the tpl argument. +type RouteGroup struct { + prefix string + router *mux.Router +} + +// ANY maps an HTTP Any request to the path and the specified handler. +func (r *RouteGroup) ANY(path string, handler http.HandlerFunc) { + r.router.PathPrefix(r.prefix).Path(path).HandlerFunc(handler) +} + +// GET maps an HTTP Get request to the path and the specified handler. +func (r *RouteGroup) GET(path string, handler http.HandlerFunc) { + r.router.PathPrefix(r.prefix).Path(path).HandlerFunc(handler).Methods("GET") +} + +// HEAD maps an HTTP Head request to the path and the specified handler. +func (r *RouteGroup) HEAD(path string, handler http.HandlerFunc) { + r.router.PathPrefix(r.prefix).Path(path).HandlerFunc(handler).Methods("HEAD") +} + +// POST maps an HTTP Post request to the path and the specified handler. +func (r *RouteGroup) POST(path string, handler http.HandlerFunc) { + r.router.PathPrefix(r.prefix).Path(path).HandlerFunc(handler).Methods("POST") +} + +// PUT maps an HTTP Put request to the path and the specified handler. +func (r *RouteGroup) PUT(path string, handler http.HandlerFunc) { + r.router.PathPrefix(r.prefix).Path(path).HandlerFunc(handler).Methods("PUT") +} + +// DELETE maps an HTTP Delete request to the path and the specified handler. +func (r *RouteGroup) DELETE(path string, handler http.HandlerFunc) { + r.router.PathPrefix(r.prefix).Path(path).HandlerFunc(handler).Methods("DELETE") +} + +// PATCH maps an HTTP Patch request to the path and the specified handler. +func (r *RouteGroup) PATCH(path string, handler http.HandlerFunc) { + r.router.PathPrefix(r.prefix).Path(path).HandlerFunc(handler).Methods("PATCH") +} + +// OPTIONS maps an HTTP Options request to the path and the specified handler. +func (r *RouteGroup) OPTIONS(path string, handler http.HandlerFunc) { + r.router.PathPrefix(r.prefix).Path(path).HandlerFunc(handler).Methods("OPTIONS") +} diff --git a/transport/http/server.go b/transport/http/server.go new file mode 100644 index 000000000..8744ef420 --- /dev/null +++ b/transport/http/server.go @@ -0,0 +1,162 @@ +package http + +import ( + "context" + "fmt" + "net" + "net/http" + "time" + + "github.com/go-kratos/kratos/v2/internal/host" + "github.com/go-kratos/kratos/v2/log" + "github.com/go-kratos/kratos/v2/middleware" + "github.com/go-kratos/kratos/v2/middleware/recovery" + "github.com/go-kratos/kratos/v2/transport" + + "github.com/gorilla/mux" +) + +const loggerName = "transport/http" + +var _ transport.Server = (*Server)(nil) + +// DecodeRequestFunc deocder request func. +type DecodeRequestFunc func(req *http.Request, v interface{}) error + +// EncodeResponseFunc is encode response func. +type EncodeResponseFunc func(res http.ResponseWriter, req *http.Request, v interface{}) error + +// EncodeErrorFunc is encode error func. +type EncodeErrorFunc func(res http.ResponseWriter, req *http.Request, err error) + +// ServerOption is HTTP server option. +type ServerOption func(*Server) + +// Network with server network. +func Network(network string) ServerOption { + return func(s *Server) { + s.network = network + } +} + +// Address with server address. +func Address(addr string) ServerOption { + return func(s *Server) { + s.address = addr + } +} + +// Timeout with server timeout. +func Timeout(timeout time.Duration) ServerOption { + return func(s *Server) { + s.timeout = timeout + } +} + +// Logger with server logger. +func Logger(logger log.Logger) ServerOption { + return func(s *Server) { + s.log = log.NewHelper(loggerName, logger) + } +} + +// Middleware with server middleware option. +func Middleware(m middleware.Middleware) ServerOption { + return func(s *Server) { + s.middleware = m + } +} + +// ErrorEncoder with error handler option. +func ErrorEncoder(fn EncodeErrorFunc) ServerOption { + return func(s *Server) { + s.errorEncoder = fn + } +} + +// Server is a HTTP server wrapper. +type Server struct { + *http.Server + lis net.Listener + network string + address string + timeout time.Duration + middleware middleware.Middleware + requestDecoder DecodeRequestFunc + responseEncoder EncodeResponseFunc + errorEncoder EncodeErrorFunc + router *mux.Router + log *log.Helper +} + +// NewServer creates a HTTP server by options. +func NewServer(opts ...ServerOption) *Server { + srv := &Server{ + network: "tcp", + address: ":0", + timeout: time.Second, + requestDecoder: defaultRequestDecoder, + responseEncoder: defaultResponseEncoder, + errorEncoder: defaultErrorEncoder, + middleware: recovery.Recovery(), + log: log.NewHelper(loggerName, log.DefaultLogger), + } + for _, o := range opts { + o(srv) + } + srv.router = mux.NewRouter() + srv.Server = &http.Server{Handler: srv} + return srv +} + +// RouteGroup . +func (s *Server) RouteGroup(prefix string) *RouteGroup { + return &RouteGroup{prefix: prefix, router: s.router} +} + +// Handle registers a new route with a matcher for the URL path. +func (s *Server) Handle(path string, h http.Handler) { + s.router.Handle(path, h) +} + +// HandleFunc registers a new route with a matcher for the URL path. +func (s *Server) HandleFunc(path string, h http.HandlerFunc) { + s.router.HandleFunc(path, h) +} + +// ServeHTTP should write reply headers and data to the ResponseWriter and then return. +func (s *Server) ServeHTTP(res http.ResponseWriter, req *http.Request) { + ctx, cancel := context.WithTimeout(req.Context(), s.timeout) + defer cancel() + ctx = transport.NewContext(ctx, transport.Transport{Kind: "HTTP"}) + ctx = NewServerContext(ctx, ServerInfo{Request: req, Response: res}) + s.router.ServeHTTP(res, req.WithContext(ctx)) +} + +// Endpoint return a real address to registry endpoint. +// examples: +// http://127.0.0.1:8000?isSecure=false +func (s *Server) Endpoint() (string, error) { + addr, err := host.Extract(s.address, s.lis) + if err != nil { + return "", err + } + return fmt.Sprintf("http://%s", addr), nil +} + +// Start start the HTTP server. +func (s *Server) Start() error { + lis, err := net.Listen(s.network, s.address) + if err != nil { + return err + } + s.lis = lis + s.log.Infof("[HTTP] server listening on: %s", lis.Addr().String()) + return s.Serve(lis) +} + +// Stop stop the HTTP server. +func (s *Server) Stop() error { + s.log.Info("[HTTP] server stopping") + return s.Shutdown(context.Background()) +} diff --git a/transport/http/server_test.go b/transport/http/server_test.go new file mode 100644 index 000000000..d8a2671d7 --- /dev/null +++ b/transport/http/server_test.go @@ -0,0 +1,79 @@ +package http + +import ( + "context" + "encoding/json" + "errors" + "fmt" + "net/http" + "testing" + "time" + + "github.com/go-kratos/kratos/v2/internal/host" +) + +type testData struct { + Path string `json:"path"` +} + +func TestServer(t *testing.T) { + fn := func(w http.ResponseWriter, r *http.Request) { + data := &testData{Path: r.RequestURI} + json.NewEncoder(w).Encode(data) + } + srv := NewServer() + group := srv.RouteGroup("/test") + { + group.GET("/", fn) + group.HEAD("/index", fn) + group.OPTIONS("/home", fn) + group.PUT("/products/{id}", fn) + group.POST("/products/{id}", fn) + group.PATCH("/products/{id}", fn) + group.DELETE("/products/{id}", fn) + } + + time.AfterFunc(time.Second, func() { + defer srv.Stop() + testClient(t, srv) + }) + + if err := srv.Start(); !errors.Is(err, http.ErrServerClosed) { + t.Fatal(err) + } +} + +func testClient(t *testing.T, srv *Server) { + tests := []struct { + method string + path string + }{ + {"GET", "/test/"}, + {"PUT", "/test/products/1"}, + {"POST", "/test/products/2"}, + {"PATCH", "/test/products/3"}, + {"DELETE", "/test/products/4"}, + } + client, err := NewClient(context.Background()) + if err != nil { + t.Fatal(err) + } + port, ok := host.Port(srv.lis) + if !ok { + t.Fatalf("extract port error: %v", srv.lis) + } + for _, test := range tests { + var res testData + url := fmt.Sprintf("http://127.0.0.1:%d%s", port, test.path) + req, err := http.NewRequest(test.method, url, nil) + if err != nil { + t.Fatal(err) + } + if err := Do(client, req, &res); err != nil { + t.Fatal(err) + } + if res.Path != test.path { + t.Errorf("expected %s got %s", test.path, res.Path) + } + } +} diff --git a/transport/http/service.go b/transport/http/service.go new file mode 100644 index 000000000..9a09847ff --- /dev/null +++ b/transport/http/service.go @@ -0,0 +1,51 @@ +package http + +import ( + "context" + "net/http" + + "github.com/go-kratos/kratos/v2/middleware" +) + +// SupportPackageIsVersion1 These constants should not be referenced from any other code. +const SupportPackageIsVersion1 = true + +type methodHandler func(srv interface{}, ctx context.Context, req *http.Request, dec func(interface{}) error, m middleware.Middleware) (out interface{}, err error) + +// MethodDesc represents a Proto service's method specification. +type MethodDesc struct { + Path string + Method string + Handler methodHandler +} + +// ServiceDesc represents a Proto service's specification. +type ServiceDesc struct { + ServiceName string + Methods []MethodDesc + Metadata interface{} +} + +// ServiceRegistrar wraps a single method that supports service registration. +type ServiceRegistrar interface { + RegisterService(desc *ServiceDesc, impl interface{}) +} + +// RegisterService . +func (s *Server) RegisterService(desc *ServiceDesc, impl interface{}) { + for _, m := range desc.Methods { + h := m.Handler + s.router.HandleFunc(m.Path, func(res http.ResponseWriter, req *http.Request) { + out, err := h(impl, req.Context(), req, func(v interface{}) error { + return s.requestDecoder(req, v) + }, s.middleware) + if err != nil { + s.errorEncoder(res, req, err) + return + } + if err := s.responseEncoder(res, req, out); err != nil { + s.errorEncoder(res, req, err) + } + }).Methods(m.Method) + } +} diff --git a/transport/http/service_test.go b/transport/http/service_test.go new file mode 100644 index 000000000..d263249d1 --- /dev/null +++ b/transport/http/service_test.go @@ -0,0 +1,98 @@ +package http + +import ( + "bytes" + "context" + "encoding/json" + "errors" + "fmt" + "net/http" + "testing" + "time" + + "github.com/go-kratos/kratos/v2/internal/host" + "github.com/go-kratos/kratos/v2/middleware" +) + +type testRequest struct { + Name string `json:"name"` +} +type testReply struct { + Result string `json:"result"` +} +type testService struct{} + +func (s *testService) SayHello(ctx context.Context, req *testRequest) (*testReply, error) { + return &testReply{Result: req.Name}, nil +} + +func TestService(t *testing.T) { + h := func(srv interface{}, ctx context.Context, req *http.Request, dec func(interface{}) error, m middleware.Middleware) (interface{}, error) { + var in testRequest + if err := dec(&in); err != nil { + return nil, err + } + h := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(*testService).SayHello(ctx, &in) + } + out, err := m(h)(ctx, &in) + if err != nil { + return nil, err + } + return out, nil + } + sd := &ServiceDesc{ + ServiceName: "helloworld.Greeter", + Methods: []MethodDesc{ + { + Path: "/helloworld", + Method: "POST", + Handler: h, + }, + }, + } + + svc := &testService{} + srv := NewServer() + srv.RegisterService(sd, svc) + + time.AfterFunc(time.Second, func() { + defer srv.Stop() + testServiceClient(t, srv) + }) + + if err := srv.Start(); !errors.Is(err, http.ErrServerClosed) { + t.Fatal(err) + } +} + +func testServiceClient(t *testing.T, srv *Server) { + client, err := NewClient(context.Background()) + if err != nil { + t.Fatal(err) + } + port, ok := host.Port(srv.lis) + if !ok { + t.Fatalf("extract port error: %v", srv.lis) + } + var ( + in = testRequest{Name: "hello"} + out = testReply{} + url = fmt.Sprintf("http://127.0.0.1:%d/helloworld", port) + ) + data, err := json.Marshal(in) + if err != nil { + t.Fatal(err) + } + req, err := http.NewRequest("POST", url, bytes.NewReader(data)) + if err != nil { + t.Fatal(err) + } + req.Header.Set("content-type", "application/json") + if err := Do(client, req, &out); err != nil { + t.Fatal(err) + } + if out.Result != in.Name { + t.Fatalf("expected %s got %s", in.Name, out.Result) + } +} diff --git a/transport/transport.go b/transport/transport.go new file mode 100644 index 000000000..64e74734a --- /dev/null +++ b/transport/transport.go @@ -0,0 +1,34 @@ +package transport + +import ( + "context" + + // init encoding + _ "github.com/go-kratos/kratos/v2/encoding/json" + _ "github.com/go-kratos/kratos/v2/encoding/proto" +) + +// Server is transport server. +type Server interface { + Endpoint() (string, error) + Start() error + Stop() error +} + +// Transport is transport context value. +type Transport struct { + Kind string +} + +type transportKey struct{} + +// NewContext returns a new Context that carries value. +func NewContext(ctx context.Context, tr Transport) context.Context { + return context.WithValue(ctx, transportKey{}, tr) +} + +// FromContext returns the Transport value stored in ctx, if any. +func FromContext(ctx context.Context) (tr Transport, ok bool) { + tr, ok = ctx.Value(transportKey{}).(Transport) + return +}