1
0
mirror of https://github.com/go-micro/go-micro.git synced 2025-07-12 22:41:07 +02:00

[feature] dashboard (#2361)

This commit is contained in:
Johnson C
2021-11-24 11:27:23 +08:00
committed by GitHub
parent 90b3e4af0b
commit 70d2bd8a6b
32 changed files with 2606 additions and 0 deletions

15
cmd/dashboard/README.md Normal file
View File

@ -0,0 +1,15 @@
# Dashboard
## Installation
```
go install github.com/asim/go-micro/cmd/dashboard/v4@latest
```
## Usage
```
dashboard --registry etcd --server_address :4000
```
Visit: [http://localhost:4000](http://localhost:4000)(deafult admin@123456)

View File

@ -0,0 +1,37 @@
package config
import "time"
type Config struct {
Server ServerConfig
}
type ServerConfig struct {
Address string
Auth AuthConfig
CORS CORSConfig
}
type AuthConfig struct {
Username string
Password string
TokenSecret string
TokenExpiration time.Duration
}
type CORSConfig struct {
Enable bool `toml:"enable"`
Origin string `toml:"origin"`
}
func GetConfig() Config {
return *_cfg
}
func GetServerConfig() ServerConfig {
return _cfg.Server
}
func GetAuthConfig() AuthConfig {
return _cfg.Server.Auth
}

View File

@ -0,0 +1,87 @@
package config
import (
"os"
"strings"
"time"
"github.com/asim/go-micro/cmd/dashboard/v4/util"
"github.com/asim/go-micro/plugins/config/encoder/toml/v4"
"github.com/asim/go-micro/plugins/config/encoder/yaml/v4"
"github.com/pkg/errors"
"go-micro.dev/v4/config"
"go-micro.dev/v4/config/reader"
"go-micro.dev/v4/config/reader/json"
"go-micro.dev/v4/config/source/env"
"go-micro.dev/v4/config/source/file"
"go-micro.dev/v4/logger"
)
// internal instance of Config
var _cfg *Config = &Config{
Server: ServerConfig{
Auth: AuthConfig{
Username: "admin",
Password: "123456",
TokenSecret: "modifyme",
TokenExpiration: 24 * time.Hour,
},
},
}
// Load will load configurations and update it when changed
func Load() error {
var configor config.Config
var err error
switch strings.ToLower(os.Getenv("CONFIG_TYPE")) {
case "toml":
filename := "config.toml"
if name := os.Getenv("CONFIG_FILE"); len(name) > 0 {
filename = name
}
configor, err = config.NewConfig(
config.WithSource(file.NewSource(file.WithPath(filename))),
config.WithReader(json.NewReader(reader.WithEncoder(toml.NewEncoder()))),
)
case "yaml":
filename := "config.yaml"
if name := os.Getenv("CONFIG_FILE"); len(name) > 0 {
filename = name
}
configor, err = config.NewConfig(
config.WithSource(file.NewSource(file.WithPath(filename))),
config.WithReader(json.NewReader(reader.WithEncoder(yaml.NewEncoder()))),
)
default:
configor, err = config.NewConfig(
config.WithSource(env.NewSource()),
)
}
if err != nil {
return errors.Wrap(err, "configor.New")
}
if err := configor.Load(); err != nil {
return errors.Wrap(err, "configor.Load")
}
if err := configor.Scan(_cfg); err != nil {
return errors.Wrap(err, "configor.Scan")
}
w, err := configor.Watch()
if err != nil {
return errors.Wrap(err, "configor.Watch")
}
util.GoSafe(func() {
for {
v, err := w.Next()
if err != nil {
logger.Error(err)
return
}
if err := v.Scan(_cfg); err != nil {
logger.Error(err)
return
}
}
})
return nil
}

125
cmd/dashboard/go.mod Normal file
View File

@ -0,0 +1,125 @@
module github.com/asim/go-micro/cmd/dashboard/v4
go 1.17
require (
github.com/asim/go-micro/plugins/client/grpc/v4 v4.0.0-20211118090700-90b3e4af0b58
github.com/asim/go-micro/plugins/client/http/v4 v4.0.0-20211118090700-90b3e4af0b58
github.com/asim/go-micro/plugins/client/mucp/v4 v4.0.0-20211118090700-90b3e4af0b58
github.com/asim/go-micro/plugins/config/encoder/toml/v4 v4.0.0-20211118090700-90b3e4af0b58
github.com/asim/go-micro/plugins/config/encoder/yaml/v4 v4.0.0-20211118090700-90b3e4af0b58
github.com/asim/go-micro/plugins/registry/consul/v4 v4.0.0-20211118090700-90b3e4af0b58
github.com/asim/go-micro/plugins/registry/etcd/v4 v4.0.0-20211118090700-90b3e4af0b58
github.com/asim/go-micro/plugins/registry/eureka/v4 v4.0.0-20211118090700-90b3e4af0b58
github.com/asim/go-micro/plugins/registry/gossip/v4 v4.0.0-20211118090700-90b3e4af0b58
github.com/asim/go-micro/plugins/registry/kubernetes/v4 v4.0.0-20211118090700-90b3e4af0b58
github.com/asim/go-micro/plugins/registry/nacos/v4 v4.0.0-20211118090700-90b3e4af0b58
github.com/asim/go-micro/plugins/registry/nats/v4 v4.0.0-20211118090700-90b3e4af0b58
github.com/asim/go-micro/plugins/registry/zookeeper/v4 v4.0.0-20211118090700-90b3e4af0b58
github.com/asim/go-micro/plugins/server/http/v4 v4.0.0-20211118090700-90b3e4af0b58
github.com/dgrijalva/jwt-go v3.2.0+incompatible
github.com/gin-gonic/gin v1.7.6
github.com/pkg/errors v0.9.1
go-micro.dev/v4 v4.4.0
golang.org/x/net v0.0.0-20210510120150-4163338589ed
)
require (
github.com/BurntSushi/toml v0.3.1 // indirect
github.com/Microsoft/go-winio v0.5.0 // indirect
github.com/ProtonMail/go-crypto v0.0.0-20210428141323-04723f9f07d7 // indirect
github.com/acomagu/bufpipe v1.0.3 // indirect
github.com/aliyun/alibaba-cloud-sdk-go v1.61.976 // indirect
github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da // indirect
github.com/bitly/go-simplejson v0.5.0 // indirect
github.com/buger/jsonparser v0.0.0-20181115193947-bf1c66bbce23 // indirect
github.com/cenkalti/backoff v2.2.1+incompatible // indirect
github.com/clbanning/x2j v0.0.0-20191024224557-825249438eec // indirect
github.com/coreos/go-semver v0.3.0 // indirect
github.com/coreos/go-systemd/v22 v22.3.2 // indirect
github.com/cpuguy83/go-md2man/v2 v2.0.0 // indirect
github.com/emirpasic/gods v1.12.0 // indirect
github.com/fatih/color v1.9.0 // indirect
github.com/franela/goreq v0.0.0-20171204163338-bcd34c9993f8 // indirect
github.com/fsnotify/fsnotify v1.4.9 // indirect
github.com/ghodss/yaml v1.0.0 // indirect
github.com/gin-contrib/sse v0.1.0 // indirect
github.com/go-errors/errors v1.0.1 // indirect
github.com/go-git/gcfg v1.5.0 // indirect
github.com/go-git/go-billy/v5 v5.3.1 // indirect
github.com/go-git/go-git/v5 v5.4.2 // indirect
github.com/go-playground/locales v0.13.0 // indirect
github.com/go-playground/universal-translator v0.17.0 // indirect
github.com/go-playground/validator/v10 v10.4.1 // indirect
github.com/go-zookeeper/zk v1.0.2 // indirect
github.com/gogo/protobuf v1.3.2 // indirect
github.com/golang/protobuf v1.5.2 // indirect
github.com/google/btree v1.0.0 // indirect
github.com/google/uuid v1.2.0 // indirect
github.com/hashicorp/consul/api v1.9.0 // indirect
github.com/hashicorp/errwrap v1.0.0 // indirect
github.com/hashicorp/go-cleanhttp v0.5.1 // indirect
github.com/hashicorp/go-hclog v0.12.0 // indirect
github.com/hashicorp/go-immutable-radix v1.0.0 // indirect
github.com/hashicorp/go-msgpack v0.5.3 // indirect
github.com/hashicorp/go-multierror v1.1.0 // indirect
github.com/hashicorp/go-rootcerts v1.0.2 // indirect
github.com/hashicorp/go-sockaddr v1.0.0 // indirect
github.com/hashicorp/golang-lru v0.5.1 // indirect
github.com/hashicorp/memberlist v0.2.2 // indirect
github.com/hashicorp/serf v0.9.5 // indirect
github.com/hudl/fargo v1.3.0 // indirect
github.com/imdario/mergo v0.3.12 // indirect
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect
github.com/jmespath/go-jmespath v0.4.0 // indirect
github.com/json-iterator/go v1.1.11 // indirect
github.com/kevinburke/ssh_config v0.0.0-20201106050909-4977a11b4351 // indirect
github.com/leodido/go-urn v1.2.0 // indirect
github.com/lestrrat/go-file-rotatelogs v0.0.0-20180223000712-d3151e2a480f // indirect
github.com/lestrrat/go-strftime v0.0.0-20180220042222-ba3bf9c1d042 // indirect
github.com/mattn/go-colorable v0.1.8 // indirect
github.com/mattn/go-isatty v0.0.12 // indirect
github.com/miekg/dns v1.1.43 // indirect
github.com/mitchellh/go-homedir v1.1.0 // indirect
github.com/mitchellh/hashstructure v1.1.0 // indirect
github.com/mitchellh/mapstructure v1.3.3 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.1 // indirect
github.com/nacos-group/nacos-sdk-go/v2 v2.0.0-Beta.1 // indirect
github.com/nats-io/jwt v1.1.0 // indirect
github.com/nats-io/nats.go v1.10.0 // indirect
github.com/nats-io/nkeys v0.1.4 // indirect
github.com/nats-io/nuid v1.0.1 // indirect
github.com/nxadm/tail v1.4.8 // indirect
github.com/op/go-logging v0.0.0-20160315200505-970db520ece7 // indirect
github.com/oxtoacart/bpool v0.0.0-20190530202638-03653db5a59c // indirect
github.com/patrickmn/go-cache v2.1.0+incompatible // indirect
github.com/russross/blackfriday/v2 v2.0.1 // indirect
github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529 // indirect
github.com/sergi/go-diff v1.1.0 // indirect
github.com/shurcooL/sanitized_anchor_name v1.0.0 // indirect
github.com/toolkits/concurrent v0.0.0-20150624120057-a4371d70e3e3 // indirect
github.com/ugorji/go/codec v1.1.7 // indirect
github.com/urfave/cli/v2 v2.3.0 // indirect
github.com/xanzy/ssh-agent v0.3.0 // indirect
go.etcd.io/etcd/api/v3 v3.5.0 // indirect
go.etcd.io/etcd/client/pkg/v3 v3.5.0 // indirect
go.etcd.io/etcd/client/v3 v3.5.0 // indirect
go.uber.org/atomic v1.7.0 // indirect
go.uber.org/multierr v1.6.0 // indirect
go.uber.org/zap v1.17.0 // indirect
golang.org/x/crypto v0.0.0-20210513164829-c07d793c2f9a // indirect
golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d // indirect
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c // indirect
golang.org/x/sys v0.0.0-20210603081109-ebe580a85c40 // indirect
golang.org/x/text v0.3.6 // indirect
google.golang.org/appengine v1.6.5 // indirect
google.golang.org/genproto v0.0.0-20210602131652-f16073e35f0c // indirect
google.golang.org/grpc v1.42.0 // indirect
google.golang.org/protobuf v1.27.1 // indirect
gopkg.in/gcfg.v1 v1.2.3 // indirect
gopkg.in/ini.v1 v1.62.0 // indirect
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 // indirect
gopkg.in/warnings.v0 v0.1.2 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect
)

1122
cmd/dashboard/go.sum Normal file

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,83 @@
package account
import (
"time"
"github.com/asim/go-micro/cmd/dashboard/v4/config"
"github.com/asim/go-micro/cmd/dashboard/v4/handler/route"
"github.com/dgrijalva/jwt-go"
"github.com/gin-gonic/gin"
"github.com/gin-gonic/gin/render"
)
type service struct{}
func NewRouteRegistrar() route.Registrar {
return service{}
}
func (s service) RegisterAuthRoute(router gin.IRoutes) {
router.GET("/api/account/profile", s.Profile)
}
func (s service) RegisterNonAuthRoute(router gin.IRoutes) {
router.POST("/api/account/login", s.Login)
}
type loginRequest struct {
Username string `json:"username" binding:"required"`
Password string `json:"password" binding:"required"`
}
type loginResponse struct {
Token string `json:"token" binding:"required"`
}
// @Tags Account
// @ID account_login
// @Param input body loginRequest true "request"
// @Success 200 {object} loginResponse "success"
// @Failure 400 {object} string
// @Failure 401 {object} string
// @Failure 500 {object} string
// @Router /api/account/login [post]
func (s *service) Login(ctx *gin.Context) {
var req loginRequest
if err := ctx.ShouldBindJSON(&req); nil != err {
ctx.Render(400, render.String{Format: err.Error()})
return
}
if req.Username != config.GetServerConfig().Auth.Username ||
req.Password != config.GetServerConfig().Auth.Password {
ctx.Render(400, render.String{Format: "incorrect username or password"})
return
}
claims := jwt.StandardClaims{
Subject: req.Username,
IssuedAt: time.Now().Unix(),
ExpiresAt: time.Now().Add(config.GetAuthConfig().TokenExpiration).Unix(),
}
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
signedToken, err := token.SignedString([]byte(config.GetAuthConfig().TokenSecret))
if err != nil {
ctx.Render(400, render.String{Format: err.Error()})
return
}
ctx.JSON(200, loginResponse{Token: signedToken})
}
type profileResponse struct {
Name string `json:"name"`
}
// @Security ApiKeyAuth
// @Tags Account
// @ID account_profile
// @Success 200 {object} profileResponse "success"
// @Failure 400 {object} string
// @Failure 401 {object} string
// @Failure 500 {object} string
// @Router /api/account/profile [get]
func (s *service) Profile(ctx *gin.Context) {
ctx.JSON(200, profileResponse{Name: config.GetAuthConfig().Username})
}

View File

@ -0,0 +1,9 @@
package client
type callRequest struct {
Service string `json:"service" binding:"required"`
Version string `json:"version"`
Endpoint string `json:"endpoint" binding:"required"`
Request string `json:"request"`
Timeout int64 `json:"timeout"`
}

View File

@ -0,0 +1,106 @@
package client
import (
"context"
"encoding/json"
"time"
"github.com/asim/go-micro/cmd/dashboard/v4/handler/route"
cgrpc "github.com/asim/go-micro/plugins/client/grpc/v4"
chttp "github.com/asim/go-micro/plugins/client/http/v4"
cmucp "github.com/asim/go-micro/plugins/client/mucp/v4"
"github.com/gin-gonic/gin"
"github.com/gin-gonic/gin/render"
"go-micro.dev/v4/client"
"go-micro.dev/v4/errors"
"go-micro.dev/v4/registry"
"go-micro.dev/v4/selector"
)
type service struct {
client client.Client
registry registry.Registry
}
func NewRouteRegistrar(client client.Client, registry registry.Registry) route.Registrar {
return service{client: client, registry: registry}
}
func (s service) RegisterAuthRoute(router gin.IRoutes) {
router.POST("/api/client/endpoint/call", s.CallEndpoint)
}
func (s service) RegisterNonAuthRoute(router gin.IRoutes) {
}
// @Security ApiKeyAuth
// @Tags Client
// @ID client_callEndpoint
// @Param input body callRequest true "request"
// @Success 200 {object} object "success"
// @Failure 400 {object} string
// @Failure 401 {object} string
// @Failure 500 {object} string
// @Router /api/client/endpoint/call [post]
func (s *service) CallEndpoint(ctx *gin.Context) {
var req callRequest
if err := ctx.ShouldBindJSON(&req); nil != err {
ctx.Render(400, render.String{Format: err.Error()})
return
}
var callReq json.RawMessage
if len(req.Request) > 0 {
if err := json.Unmarshal([]byte(req.Request), &callReq); err != nil {
ctx.Render(400, render.String{Format: "parse request failed: %s", Data: []interface{}{err.Error()}})
return
}
}
services, err := s.registry.GetService(req.Service)
if err != nil {
ctx.Render(400, render.String{Format: err.Error()})
return
}
var c client.Client
for _, srv := range services {
if len(req.Version) > 0 && req.Version != srv.Version {
continue
}
if len(srv.Nodes) == 0 {
ctx.Render(400, render.String{Format: "service node not found"})
return
}
switch srv.Nodes[0].Metadata["server"] {
case "grpc":
c = cgrpc.NewClient()
case "http":
c = chttp.NewClient()
case "mucp":
c = cmucp.NewClient()
default:
c = s.client
}
break
}
if c == nil {
ctx.Render(400, render.String{Format: "service not found"})
return
}
var resp json.RawMessage
callOpts := []client.CallOption{}
if len(req.Version) > 0 {
callOpts = append(callOpts, client.WithSelectOption(selector.WithFilter(selector.FilterVersion(req.Version))))
}
requestOpts := []client.RequestOption{client.WithContentType("application/json")}
if req.Timeout > 0 {
callOpts = append(callOpts, client.WithRequestTimeout(time.Duration(req.Timeout)*time.Second))
}
if err := c.Call(context.TODO(), client.NewRequest(req.Service, req.Endpoint, callReq, requestOpts...), &resp, callOpts...); err != nil {
if merr := errors.Parse(err.Error()); merr != nil {
ctx.JSON(200, gin.H{"success": false, "error": merr})
} else {
ctx.JSON(200, gin.H{"success": false, "error": err.Error})
}
return
}
ctx.JSON(200, resp)
}

View File

@ -0,0 +1,39 @@
package handler
import (
"github.com/asim/go-micro/cmd/dashboard/v4/config"
"github.com/asim/go-micro/cmd/dashboard/v4/handler/account"
handlerclient "github.com/asim/go-micro/cmd/dashboard/v4/handler/client"
"github.com/asim/go-micro/cmd/dashboard/v4/handler/registry"
"github.com/asim/go-micro/cmd/dashboard/v4/handler/route"
"github.com/asim/go-micro/cmd/dashboard/v4/handler/statistics"
"github.com/asim/go-micro/cmd/dashboard/v4/web"
"github.com/gin-gonic/gin"
"go-micro.dev/v4/client"
)
type Options struct {
Client client.Client
Router *gin.Engine
}
func Register(opts Options) error {
router := opts.Router
if err := web.RegisterRoute(router); err != nil {
return err
}
if cfg := config.GetServerConfig().CORS; cfg.Enable {
router.Use(CorsHandler(cfg.Origin))
}
authRouter := router.Group("").Use(AuthRequired())
for _, r := range []route.Registrar{
account.NewRouteRegistrar(),
handlerclient.NewRouteRegistrar(opts.Client, opts.Client.Options().Registry),
registry.NewRouteRegistrar(opts.Client.Options().Registry),
statistics.NewRouteRegistrar(opts.Client.Options().Registry),
} {
r.RegisterNonAuthRoute(router)
r.RegisterAuthRoute(authRouter)
}
return nil
}

View File

@ -0,0 +1,51 @@
package handler
import (
"net/http"
"strings"
"github.com/asim/go-micro/cmd/dashboard/v4/config"
"github.com/dgrijalva/jwt-go"
"github.com/gin-gonic/gin"
)
func AuthRequired() gin.HandlerFunc {
return func(ctx *gin.Context) {
if ctx.Request.Method == "OPTIONS" {
ctx.Next()
return
}
tokenString := ctx.GetHeader("Authorization")
if len(tokenString) == 0 || !strings.HasPrefix(tokenString, "Bearer ") {
ctx.AbortWithStatusJSON(http.StatusUnauthorized, "")
return
}
tokenString = tokenString[7:]
claims := jwt.StandardClaims{}
token, err := jwt.ParseWithClaims(tokenString, &claims, func(t *jwt.Token) (interface{}, error) {
return []byte(config.GetAuthConfig().TokenSecret), nil
})
if err != nil {
ctx.AbortWithError(http.StatusUnauthorized, err)
}
if !token.Valid {
ctx.AbortWithStatus(http.StatusUnauthorized)
}
ctx.Set("username", claims.Subject)
ctx.Next()
}
}
func CorsHandler(allowOrigin string) gin.HandlerFunc {
return func(ctx *gin.Context) {
ctx.Header("Access-Control-Allow-Origin", allowOrigin)
ctx.Header("Access-Control-Allow-Headers", "Content-Type, Authorization, token")
ctx.Header("Access-Control-Allow-Methods", "POST, GET, DELETE, PUT, OPTIONS")
ctx.Header("Access-Control-Expose-Headers", "Content-Length, Access-Control-Allow-Origin, Access-Control-Allow-Headers, Content-Type")
ctx.Header("Access-Control-Allow-Credentials", "true")
if ctx.Request.Method == "OPTIONS" {
ctx.AbortWithStatus(http.StatusNoContent)
}
ctx.Next()
}
}

View File

@ -0,0 +1,62 @@
package registry
import "go-micro.dev/v4/registry"
type registryServiceSummary struct {
Name string `json:"name" binding:"required"`
Versions []string `json:"versions,omitempty"`
}
type getServiceListResponse struct {
Services []registryServiceSummary `json:"services" binding:"required"`
}
type registryService struct {
Name string `json:"name" binding:"required"`
Version string `json:"version" binding:"required"`
Metadata map[string]string `json:"metadata,omitempty"`
Endpoints []registryEndpoint `json:"endpoints,omitempty"`
Nodes []registryNode `json:"nodes,omitempty"`
}
type registryEndpoint struct {
Name string `json:"name" binding:"required"`
Request registryValue `json:"request" binding:"required"`
Response registryValue `json:"response"`
Metadata map[string]string `json:"metadata,omitempty"`
}
type registryNode struct {
Id string `json:"id" binding:"required"`
Address string `json:"address" binding:"required"`
Metadata map[string]string `json:"metadata,omitempty"`
}
type registryValue struct {
Name string `json:"name" binding:"required"`
Type string `json:"type" binding:"required"`
Values []registryValue `json:"values,omitempty"`
}
type getServiceDetailResponse struct {
Services []registryService `json:"services"`
}
type getServiceEndpointsResponse struct {
Endpoints []registryEndpoint `json:"endpoints"`
}
func convertRegistryValue(v *registry.Value) registryValue {
if v == nil {
return registryValue{}
}
res := registryValue{
Name: v.Name,
Type: v.Type,
Values: make([]registryValue, 0, len(v.Values)),
}
for _, vv := range v.Values {
res.Values = append(res.Values, convertRegistryValue(vv))
}
return res
}

View File

@ -0,0 +1,161 @@
package registry
import (
"sort"
"github.com/asim/go-micro/cmd/dashboard/v4/handler/route"
"github.com/gin-gonic/gin"
"github.com/gin-gonic/gin/render"
"go-micro.dev/v4/registry"
)
type service struct {
registry registry.Registry
}
func NewRouteRegistrar(registry registry.Registry) route.Registrar {
return service{registry: registry}
}
func (s service) RegisterAuthRoute(router gin.IRoutes) {
router.GET("/api/registry/services", s.GetServices)
router.GET("/api/registry/service", s.GetServiceDetail)
router.GET("/api/registry/service/endpoints", s.GetServiceEndpoints)
}
func (s service) RegisterNonAuthRoute(router gin.IRoutes) {
}
// @Security ApiKeyAuth
// @Tags Registry
// @ID registry_getServices
// @Success 200 {object} getServiceListResponse
// @Failure 400 {object} string
// @Failure 401 {object} string
// @Failure 500 {object} string
// @Router /api/registry/services [get]
func (s *service) GetServices(ctx *gin.Context) {
services, err := s.registry.ListServices()
if err != nil {
ctx.Render(500, render.String{Format: err.Error()})
return
}
tmp := make(map[string][]string)
resp := getServiceListResponse{Services: make([]registryServiceSummary, 0, len(services))}
for _, s := range services {
if sr, ok := tmp[s.Name]; ok {
sr = append(sr, s.Version)
tmp[s.Name] = sr
} else {
tmp[s.Name] = []string{s.Version}
}
}
for k, v := range tmp {
sort.Strings(v)
resp.Services = append(resp.Services, registryServiceSummary{Name: k, Versions: v})
}
sort.Slice(resp.Services, func(i, j int) bool {
return resp.Services[i].Name < resp.Services[j].Name
})
ctx.JSON(200, resp)
}
// @Security ApiKeyAuth
// @Tags Registry
// @ID registry_getServiceDetail
// @Param name query string true "service name"
// @Param version query string false "service version"
// @Success 200 {object} getServiceDetailResponse
// @Failure 400 {object} string
// @Failure 401 {object} string
// @Failure 500 {object} string
// @Router /api/registry/service [get]
func (s *service) GetServiceDetail(ctx *gin.Context) {
name := ctx.Query("name")
if len(name) == 0 {
ctx.Render(400, render.String{Format: "service name required"})
return
}
services, err := s.registry.GetService(name)
if err != nil {
ctx.Render(500, render.String{Format: err.Error()})
return
}
version := ctx.Query("version")
resp := getServiceDetailResponse{Services: make([]registryService, 0, len(services))}
for _, s := range services {
if len(version) > 0 && s.Version != version {
continue
}
endpoints := make([]registryEndpoint, 0, len(s.Endpoints))
for _, e := range s.Endpoints {
endpoints = append(endpoints, registryEndpoint{
Name: e.Name,
Request: convertRegistryValue(e.Request),
Response: convertRegistryValue(e.Response),
Metadata: e.Metadata,
})
}
nodes := make([]registryNode, 0, len(s.Nodes))
for _, n := range s.Nodes {
nodes = append(nodes, registryNode{
Id: n.Id,
Address: n.Address,
Metadata: n.Metadata,
})
}
resp.Services = append(resp.Services, registryService{
Name: s.Name,
Version: s.Version,
Metadata: s.Metadata,
Endpoints: endpoints,
Nodes: nodes,
})
}
ctx.JSON(200, resp)
}
// @Security ApiKeyAuth
// @Tags Registry
// @ID registry_getServiceEndpoints
// @Param name query string true "service name"
// @Param version query string false "service version"
// @Success 200 {object} getServiceEndpointsResponse
// @Failure 400 {object} string
// @Failure 401 {object} string
// @Failure 500 {object} string
// @Router /api/registry/service/endpoints [get]
func (s *service) GetServiceEndpoints(ctx *gin.Context) {
name := ctx.Query("name")
if len(name) == 0 {
ctx.Render(400, render.String{Format: "service name required"})
return
}
services, err := s.registry.GetService(name)
if err != nil {
ctx.Render(500, render.String{Format: err.Error()})
return
}
version := ctx.Query("version")
resp := getServiceEndpointsResponse{}
for _, s := range services {
if s.Version != version {
continue
}
endpoints := make([]registryEndpoint, 0, len(s.Endpoints))
for _, e := range s.Endpoints {
if e.Name == "Func" {
continue
}
endpoints = append(endpoints, registryEndpoint{
Name: e.Name,
Request: convertRegistryValue(e.Request),
Response: convertRegistryValue(e.Response),
Metadata: e.Metadata,
})
}
resp.Endpoints = endpoints
break
}
ctx.JSON(200, resp)
}

View File

@ -0,0 +1,8 @@
package route
import "github.com/gin-gonic/gin"
type Registrar interface {
RegisterAuthRoute(gin.IRoutes)
RegisterNonAuthRoute(gin.IRoutes)
}

View File

@ -0,0 +1,16 @@
package statistics
type getSummaryResponse struct {
Registry registrySummary `json:"registry"`
Services servicesSummary `json:"services"`
}
type registrySummary struct {
Type string `json:"type"`
Addrs []string `json:"addrs"`
}
type servicesSummary struct {
Count int `json:"count"`
NodesCount int `json:"nodes_count"`
}

View File

@ -0,0 +1,56 @@
package statistics
import (
"github.com/asim/go-micro/cmd/dashboard/v4/handler/route"
"github.com/gin-gonic/gin"
"go-micro.dev/v4/registry"
)
type service struct {
registry registry.Registry
}
func NewRouteRegistrar(registry registry.Registry) route.Registrar {
return service{registry: registry}
}
func (s service) RegisterAuthRoute(router gin.IRoutes) {
router.GET("/api/summary", s.GetSummary)
}
func (s service) RegisterNonAuthRoute(router gin.IRoutes) {
}
// @Security ApiKeyAuth
// @Tags Statistics
// @ID statistics_getSummary
// @Success 200 {object} getSummaryResponse
// @Failure 400 {object} string
// @Failure 401 {object} string
// @Failure 500 {object} string
// @Router /api/summary [get]
func (s *service) GetSummary(ctx *gin.Context) {
services, err := s.registry.ListServices()
if err != nil {
ctx.AbortWithStatusJSON(500, err)
}
servicesByName := make(map[string]struct{})
var servicesNodesCount int
for _, s := range services {
if _, ok := servicesByName[s.Name]; !ok {
servicesByName[s.Name] = struct{}{}
}
servicesNodesCount += len(s.Nodes)
}
var resp = getSummaryResponse{
Registry: registrySummary{
Type: s.registry.String(),
Addrs: s.registry.Options().Addrs,
},
Services: servicesSummary{
Count: len(servicesByName),
NodesCount: servicesNodesCount,
},
}
ctx.JSON(200, resp)
}

39
cmd/dashboard/main.go Normal file
View File

@ -0,0 +1,39 @@
package main
import (
"github.com/asim/go-micro/cmd/dashboard/v4/config"
"github.com/asim/go-micro/cmd/dashboard/v4/handler"
mhttp "github.com/asim/go-micro/plugins/server/http/v4"
"github.com/gin-gonic/gin"
"go-micro.dev/v4"
"go-micro.dev/v4/logger"
)
const (
Name = "go.micro.dashboard"
Version = "1.0.0"
)
func main() {
if err := config.Load(); err != nil {
logger.Fatal(err)
}
srv := micro.NewService(micro.Server(mhttp.NewServer()))
opts := []micro.Option{
micro.Name(Name),
micro.Version(Version),
}
srv.Init(opts...)
gin.SetMode(gin.ReleaseMode)
router := gin.New()
router.Use(gin.Recovery(), gin.Logger())
if err := handler.Register(handler.Options{Client: srv.Client(), Router: router}); err != nil {
logger.Fatal(err)
}
if err := micro.RegisterHandler(srv.Server(), router); err != nil {
logger.Fatal(err)
}
if err := srv.Run(); err != nil {
logger.Fatal(err)
}
}

12
cmd/dashboard/plugins.go Normal file
View File

@ -0,0 +1,12 @@
package main
import (
_ "github.com/asim/go-micro/plugins/registry/consul/v4"
_ "github.com/asim/go-micro/plugins/registry/etcd/v4"
_ "github.com/asim/go-micro/plugins/registry/eureka/v4"
_ "github.com/asim/go-micro/plugins/registry/gossip/v4"
_ "github.com/asim/go-micro/plugins/registry/kubernetes/v4"
_ "github.com/asim/go-micro/plugins/registry/nacos/v4"
_ "github.com/asim/go-micro/plugins/registry/nats/v4"
_ "github.com/asim/go-micro/plugins/registry/zookeeper/v4"
)

View File

@ -0,0 +1,22 @@
package util
import (
"runtime/debug"
"go-micro.dev/v4/logger"
)
// GoSafe will run func in goroutine safely, avoid crash from unexpected panic
func GoSafe(fn func()) {
if fn == nil {
return
}
go func() {
defer func() {
if e := recover(); e != nil {
logger.Errorf("[panic]%v\n%s", e, debug.Stack())
}
}()
fn()
}()
}

144
cmd/dashboard/web/ab0x.go Normal file
View File

@ -0,0 +1,144 @@
// Code generated by fileb0x at "2021-11-22 18:03:08.6921519 +0800 CST m=+0.057344801" from config file "b0x.yaml" DO NOT EDIT.
// modification hash(781cbd1c3197446ff0f2b64f4796afe6.8be3f833d63e3c844663716446e13a42)
package web
import (
"bytes"
"context"
"io"
"net/http"
"os"
"path"
"golang.org/x/net/webdav"
)
var (
// CTX is a context for webdav vfs
CTX = context.Background()
// FS is a virtual memory file system
FS = webdav.NewMemFS()
// Handler is used to server files through a http handler
Handler *webdav.Handler
// HTTP is the http file system
HTTP http.FileSystem = new(HTTPFS)
)
// HTTPFS implements http.FileSystem
type HTTPFS struct {
// Prefix allows to limit the path of all requests. F.e. a prefix "css" would allow only calls to /css/*
Prefix string
}
func init() {
err := CTX.Err()
if err != nil {
panic(err)
}
err = FS.Mkdir(CTX, "/assets/", 0777)
if err != nil && err != os.ErrExist {
panic(err)
}
Handler = &webdav.Handler{
FileSystem: FS,
LockSystem: webdav.NewMemLS(),
}
}
// Open a file
func (hfs *HTTPFS) Open(path string) (http.File, error) {
path = hfs.Prefix + path
f, err := FS.OpenFile(CTX, path, os.O_RDONLY, 0644)
if err != nil {
return nil, err
}
return f, nil
}
// ReadFile is adapTed from ioutil
func ReadFile(path string) ([]byte, error) {
f, err := FS.OpenFile(CTX, path, os.O_RDONLY, 0644)
if err != nil {
return nil, err
}
buf := bytes.NewBuffer(make([]byte, 0, bytes.MinRead))
// 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(f)
return buf.Bytes(), err
}
// WriteFile is adapTed from ioutil
func WriteFile(filename string, data []byte, perm os.FileMode) error {
f, err := FS.OpenFile(CTX, filename, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, perm)
if err != nil {
return err
}
n, err := f.Write(data)
if err == nil && n < len(data) {
err = io.ErrShortWrite
}
if err1 := f.Close(); err == nil {
err = err1
}
return err
}
// WalkDirs looks for files in the given dir and returns a list of files in it
// usage for all files in the b0x: WalkDirs("", false)
func WalkDirs(name string, includeDirsInList bool, files ...string) ([]string, error) {
f, err := FS.OpenFile(CTX, name, os.O_RDONLY, 0)
if err != nil {
return nil, err
}
fileInfos, err := f.Readdir(0)
if err != nil {
return nil, err
}
err = f.Close()
if err != nil {
return nil, err
}
for _, info := range fileInfos {
filename := path.Join(name, info.Name())
if includeDirsInList || !info.IsDir() {
files = append(files, filename)
}
if info.IsDir() {
files, err = WalkDirs(filename, includeDirsInList, files...)
if err != nil {
return nil, err
}
}
}
return files, nil
}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,52 @@
package web
import (
"path/filepath"
"github.com/gin-gonic/gin"
)
func RegisterRoute(router *gin.Engine) error {
files, err := WalkDirs("", false)
if err != nil {
return err
}
for _, f := range files {
router.GET(f, func(name string) gin.HandlerFunc {
return func(c *gin.Context) {
data, err := ReadFile(name)
if err != nil {
c.AbortWithError(500, err)
return
}
switch filepath.Ext(name) {
case ".html":
c.Header("Content-Type", "text/html; charset=utf-8")
case ".css":
c.Header("Content-Type", "text/css; charset=utf-8")
case ".js":
c.Header("Content-Type", "application/javascript")
case ".svg":
c.Header("Content-Type", "image/svg+xml")
}
if _, err := c.Writer.Write(data); err != nil {
c.AbortWithError(500, err)
return
}
}
}(f))
}
router.GET("/", func(c *gin.Context) {
data, err := ReadFile("index.html")
if err != nil {
c.AbortWithError(500, err)
return
}
c.Header("Content-Type", "text/html; charset=utf-8")
if _, err := c.Writer.Write(data); err != nil {
c.AbortWithError(500, err)
return
}
})
return nil
}