[feat] service nodes health check

main
Johnson C 2 years ago
parent f61cf794f3
commit 7d8b6abe86

@ -1,3 +1,6 @@
### 1.3.0 2021/12/16)
> 1. Support service nodes health check, but http server type is unsupported currently.
### 1.2.0 2021/12/07)
> 1. Update login page, using new logo and icon.
> 2. Using delon auth config.

@ -4,7 +4,7 @@ import "time"
const (
Name = "go.micro.dashboard"
Version = "1.2.0"
Version = "1.3.0"
)
const (

@ -158,6 +158,56 @@ var doc = `{
}
}
},
"/api/client/healthcheck": {
"post": {
"security": [
{
"ApiKeyAuth": []
}
],
"tags": [
"Client"
],
"operationId": "client_healthCheck",
"parameters": [
{
"description": "request",
"name": "input",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/client.healthCheckRequest"
}
}
],
"responses": {
"200": {
"description": "success",
"schema": {
"type": "object"
}
},
"400": {
"description": "Bad Request",
"schema": {
"type": "string"
}
},
"401": {
"description": "Unauthorized",
"schema": {
"type": "string"
}
},
"500": {
"description": "Internal Server Error",
"schema": {
"type": "string"
}
}
}
}
},
"/api/client/publish": {
"post": {
"security": [
@ -316,6 +366,45 @@ var doc = `{
}
}
},
"/api/registry/service/nodes": {
"get": {
"security": [
{
"ApiKeyAuth": []
}
],
"tags": [
"Registry"
],
"operationId": "registry_getNodes",
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/registry.getNodeListResponse"
}
},
"400": {
"description": "Bad Request",
"schema": {
"type": "string"
}
},
"401": {
"description": "Unauthorized",
"schema": {
"type": "string"
}
},
"500": {
"description": "Internal Server Error",
"schema": {
"type": "string"
}
}
}
}
},
"/api/registry/service/subscribers": {
"get": {
"security": [
@ -521,6 +610,28 @@ var doc = `{
}
}
},
"client.healthCheckRequest": {
"type": "object",
"required": [
"address",
"service",
"version"
],
"properties": {
"address": {
"type": "string"
},
"service": {
"type": "string"
},
"timeout": {
"type": "integer"
},
"version": {
"type": "string"
}
}
},
"client.publishRequest": {
"type": "object",
"required": [
@ -536,6 +647,17 @@ var doc = `{
}
}
},
"registry.getNodeListResponse": {
"type": "object",
"properties": {
"services": {
"type": "array",
"items": {
"$ref": "#/definitions/registry.registryServiceNodes"
}
}
}
},
"registry.getServiceDetailResponse": {
"type": "object",
"properties": {
@ -604,6 +726,9 @@ var doc = `{
},
"response": {
"$ref": "#/definitions/registry.registryValue"
},
"stream": {
"type": "boolean"
}
}
},
@ -628,6 +753,31 @@ var doc = `{
}
}
},
"registry.registryNodeDetail": {
"type": "object",
"required": [
"address",
"id",
"version"
],
"properties": {
"address": {
"type": "string"
},
"id": {
"type": "string"
},
"metadata": {
"type": "object",
"additionalProperties": {
"type": "string"
}
},
"version": {
"type": "string"
}
}
},
"registry.registryService": {
"type": "object",
"required": [
@ -667,6 +817,20 @@ var doc = `{
}
}
},
"registry.registryServiceNodes": {
"type": "object",
"properties": {
"name": {
"type": "string"
},
"nodes": {
"type": "array",
"items": {
"$ref": "#/definitions/registry.registryNodeDetail"
}
}
}
},
"registry.registryServiceSummary": {
"type": "object",
"required": [
@ -762,7 +926,7 @@ type swaggerInfo struct {
// SwaggerInfo holds exported Swagger Info so clients can modify it
var SwaggerInfo = swaggerInfo{
Version: "1.2.0",
Version: "1.3.0",
Host: "",
BasePath: "/",
Schemes: []string{},

@ -5,7 +5,7 @@
"title": "Go Micro Dashboard",
"termsOfService": "http://swagger.io/terms/",
"contact": {},
"version": "1.2.0"
"version": "1.3.0"
},
"basePath": "/",
"paths": {
@ -143,6 +143,56 @@
}
}
},
"/api/client/healthcheck": {
"post": {
"security": [
{
"ApiKeyAuth": []
}
],
"tags": [
"Client"
],
"operationId": "client_healthCheck",
"parameters": [
{
"description": "request",
"name": "input",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/client.healthCheckRequest"
}
}
],
"responses": {
"200": {
"description": "success",
"schema": {
"type": "object"
}
},
"400": {
"description": "Bad Request",
"schema": {
"type": "string"
}
},
"401": {
"description": "Unauthorized",
"schema": {
"type": "string"
}
},
"500": {
"description": "Internal Server Error",
"schema": {
"type": "string"
}
}
}
}
},
"/api/client/publish": {
"post": {
"security": [
@ -301,6 +351,45 @@
}
}
},
"/api/registry/service/nodes": {
"get": {
"security": [
{
"ApiKeyAuth": []
}
],
"tags": [
"Registry"
],
"operationId": "registry_getNodes",
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/registry.getNodeListResponse"
}
},
"400": {
"description": "Bad Request",
"schema": {
"type": "string"
}
},
"401": {
"description": "Unauthorized",
"schema": {
"type": "string"
}
},
"500": {
"description": "Internal Server Error",
"schema": {
"type": "string"
}
}
}
}
},
"/api/registry/service/subscribers": {
"get": {
"security": [
@ -506,6 +595,28 @@
}
}
},
"client.healthCheckRequest": {
"type": "object",
"required": [
"address",
"service",
"version"
],
"properties": {
"address": {
"type": "string"
},
"service": {
"type": "string"
},
"timeout": {
"type": "integer"
},
"version": {
"type": "string"
}
}
},
"client.publishRequest": {
"type": "object",
"required": [
@ -521,6 +632,17 @@
}
}
},
"registry.getNodeListResponse": {
"type": "object",
"properties": {
"services": {
"type": "array",
"items": {
"$ref": "#/definitions/registry.registryServiceNodes"
}
}
}
},
"registry.getServiceDetailResponse": {
"type": "object",
"properties": {
@ -589,6 +711,9 @@
},
"response": {
"$ref": "#/definitions/registry.registryValue"
},
"stream": {
"type": "boolean"
}
}
},
@ -613,6 +738,31 @@
}
}
},
"registry.registryNodeDetail": {
"type": "object",
"required": [
"address",
"id",
"version"
],
"properties": {
"address": {
"type": "string"
},
"id": {
"type": "string"
},
"metadata": {
"type": "object",
"additionalProperties": {
"type": "string"
}
},
"version": {
"type": "string"
}
}
},
"registry.registryService": {
"type": "object",
"required": [
@ -652,6 +802,20 @@
}
}
},
"registry.registryServiceNodes": {
"type": "object",
"properties": {
"name": {
"type": "string"
},
"nodes": {
"type": "array",
"items": {
"$ref": "#/definitions/registry.registryNodeDetail"
}
}
}
},
"registry.registryServiceSummary": {
"type": "object",
"required": [

@ -38,6 +38,21 @@ definitions:
- endpoint
- service
type: object
client.healthCheckRequest:
properties:
address:
type: string
service:
type: string
timeout:
type: integer
version:
type: string
required:
- address
- service
- version
type: object
client.publishRequest:
properties:
message:
@ -48,6 +63,13 @@ definitions:
- message
- topic
type: object
registry.getNodeListResponse:
properties:
services:
items:
$ref: '#/definitions/registry.registryServiceNodes'
type: array
type: object
registry.getServiceDetailResponse:
properties:
services:
@ -90,6 +112,8 @@ definitions:
$ref: '#/definitions/registry.registryValue'
response:
$ref: '#/definitions/registry.registryValue'
stream:
type: boolean
required:
- name
- request
@ -108,6 +132,23 @@ definitions:
- address
- id
type: object
registry.registryNodeDetail:
properties:
address:
type: string
id:
type: string
metadata:
additionalProperties:
type: string
type: object
version:
type: string
required:
- address
- id
- version
type: object
registry.registryService:
properties:
handlers:
@ -134,6 +175,15 @@ definitions:
- name
- version
type: object
registry.registryServiceNodes:
properties:
name:
type: string
nodes:
items:
$ref: '#/definitions/registry.registryNodeDetail'
type: array
type: object
registry.registryServiceSummary:
properties:
name:
@ -187,7 +237,7 @@ info:
description: go micro dashboard restful-api
termsOfService: http://swagger.io/terms/
title: Go Micro Dashboard
version: 1.2.0
version: 1.3.0
paths:
/api/account/login:
post:
@ -273,6 +323,37 @@ paths:
- ApiKeyAuth: []
tags:
- Client
/api/client/healthcheck:
post:
operationId: client_healthCheck
parameters:
- description: request
in: body
name: input
required: true
schema:
$ref: '#/definitions/client.healthCheckRequest'
responses:
"200":
description: success
schema:
type: object
"400":
description: Bad Request
schema:
type: string
"401":
description: Unauthorized
schema:
type: string
"500":
description: Internal Server Error
schema:
type: string
security:
- ApiKeyAuth: []
tags:
- Client
/api/client/publish:
post:
operationId: client_publish
@ -372,6 +453,30 @@ paths:
- ApiKeyAuth: []
tags:
- Registry
/api/registry/service/nodes:
get:
operationId: registry_getNodes
responses:
"200":
description: OK
schema:
$ref: '#/definitions/registry.getNodeListResponse'
"400":
description: Bad Request
schema:
type: string
"401":
description: Unauthorized
schema:
type: string
"500":
description: Internal Server Error
schema:
type: string
security:
- ApiKeyAuth: []
tags:
- Registry
/api/registry/service/subscribers:
get:
operationId: registry_getServiceSubscribers

@ -1,6 +1,6 @@
{
"name": "go-micro-dashboard",
"version": "1.2.0",
"version": "1.3.0",
"scripts": {
"ng": "ng",
"start": "ng s -o",

@ -58,6 +58,11 @@ export class StartupService {
text: 'Services',
link: '/services',
icon: { type: 'icon', value: 'cloud' }
},
{
text: 'Nodes',
link: '/service/nodes',
icon: { type: 'icon', value: 'apartment' }
}
]
},

@ -51,6 +51,13 @@
[nzAutosize]="{ minRows: 5, maxRows: 20 }" (ngModelChange)="requestChanged($event)"></textarea>
</nz-form-control>
</nz-form-item>
<nz-form-item>
<nz-form-control>
Timeout(Seconds)
<nz-input-number name="timeout" nzPlaceHolder="timeout" [(ngModel)]="timeout" [nzMin]="1"
[nzMax]="60" [nzStep]="1"></nz-input-number>
</nz-form-control>
</nz-form-item>
<nz-form-item>
<button nz-button type="submit" nzType="primary" nzSize="large" [nzLoading]="loading" nzBlock>
Call

@ -11,8 +11,9 @@ import { ClientPublishComponent } from './client/publish.component';
import { DashboardComponent } from './dashboard/dashboard.component';
// passport pages
import { UserLoginComponent } from './passport/login/login.component';
import { ServiceDetailComponent } from './services/detail.component';
import { ServicesListComponent } from './services/list.component';
import { ServiceDetailComponent } from './services/detail.component';
import { ServiceNodesComponent } from './services/nodes.component';
const routes: Routes = [
{
@ -24,6 +25,7 @@ const routes: Routes = [
{ path: 'dashboard', component: DashboardComponent, data: { title: 'Dashboard', titleI18n: 'dashboard' } },
{ path: 'services', component: ServicesListComponent, data: { title: 'Services', titleI18n: 'services' } },
{ path: 'service/detail', component: ServiceDetailComponent },
{ path: 'service/nodes', component: ServiceNodesComponent },
{ path: 'client/call', component: ClientCallComponent, data: { title: 'Call', titleI18n: 'call' } },
{ path: 'client/publish', component: ClientPublishComponent, data: { title: 'Call', titleI18n: 'call' } },
{ path: 'exception', loadChildren: () => import('./exception/exception.module').then(m => m.ExceptionModule) },

@ -4,8 +4,9 @@ import { SharedModule } from '@shared';
import { DashboardComponent } from './dashboard/dashboard.component';
import { UserLoginComponent } from './passport/login/login.component';
import { RouteRoutingModule } from './routes-routing.module';
import { ServiceDetailComponent } from './services/detail.component';
import { ServicesListComponent } from './services/list.component';
import { ServiceDetailComponent } from './services/detail.component';
import { ServiceNodesComponent } from './services/nodes.component';
import { ClientCallComponent } from './client/call.component';
import { ClientPublishComponent } from './client/publish.component';
@ -14,6 +15,7 @@ const COMPONENTS: Array<Type<void>> = [
UserLoginComponent,
ServicesListComponent,
ServiceDetailComponent,
ServiceNodesComponent,
ClientCallComponent,
ClientPublishComponent,
];

@ -0,0 +1,54 @@
<page-header [breadcrumb]="breadcrumb" [action]="action">
<ng-template #breadcrumb>
<nz-breadcrumb>
<nz-breadcrumb-item>
<a href="#/dashboard">Dashboard</a>
</nz-breadcrumb-item>
<nz-breadcrumb-item>
<a href="#/services">Services</a>
</nz-breadcrumb-item>
<nz-breadcrumb-item>Nodes</nz-breadcrumb-item>
</nz-breadcrumb>
</ng-template>
<ng-template #action>
<button nz-button nzType="primary" style="float:right;" (click)="load()" [nzLoading]="loading">Refresh</button>
</ng-template>
</page-header>
<nz-collapse nzGhost>
<nz-collapse-panel *ngFor="let service of services" [nzHeader]="service.name" nzActive="true">
<nz-table #nodeTable *ngIf="service.nodes" nzExpandedIcon="double-right" [nzData]="service.nodes"
[nzFrontPagination]="false" [nzBordered]="true">
<thead>
<tr>
<th>Id</th>
<th>Version</th>
<th>Address</th>
<th>Metadata</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
<tr *ngFor="let data of nodeTable.data">
<td nz-tooltip [nzTooltipTitle]="data.tip">
<i *ngIf="data.valid!==undefined&&!data.valid" nz-icon [nzType]="'close-circle'"
[nzTheme]="'twotone'" [nzTwotoneColor]="'#eb2f96'"></i>
<i *ngIf="data.valid!==undefined&&data.valid" nz-icon [nzType]="'check-circle'"
[nzTheme]="'twotone'" [nzTwotoneColor]="'#52c41a'"></i>
{{data.id}}
</td>
<td>{{data.version}}</td>
<td>{{data.address}} </td>
<td>{{data.metadata|json}}</td>
<td>
<button nz-button nzType="primary" [nzLoading]="data.loading"
[disabled]="!data.metadata['server']||data.metadata['server']=='http'"
(click)="healthCheck(service.name, data)">Health</button>
</td>
</tr>
</tbody>
</nz-table>
</nz-collapse-panel>
</nz-collapse>
<nz-back-top></nz-back-top>

@ -0,0 +1,63 @@
import { ChangeDetectionStrategy, Component, OnInit } from '@angular/core';
import { ActivatedRoute, Router } from '@angular/router';
import { finalize } from 'rxjs/operators';
import { ClientServiceProxy, HealthCheckRequest, RegistryServiceProxy } from 'src/app/shared/service-proxies/service-proxies';
@Component({
selector: 'micro-nodes',
templateUrl: './nodes.component.html',
changeDetection: ChangeDetectionStrategy.Default
})
export class ServiceNodesComponent implements OnInit {
loading = false;
name = '';
version: string | null = null;
services: any;
constructor(private readonly registryService: RegistryServiceProxy,
private readonly clientService: ClientServiceProxy,
) {
}
ngOnInit(): void {
this.load();
}
load() {
this.loading = true;
this.registryService.getNodes().pipe(
finalize(() => {
this.loading = false;
})
).subscribe(resp => {
if (resp.services) {
this.services = resp.services;
}
});
}
healthCheck(service: any, data: any) {
data.loading = true;
var input = new HealthCheckRequest({
service: service,
address: data.address,
version: data.version,
});
this.clientService.healthCheck(input).pipe(
finalize(() => {
data.loading = false;
})
).subscribe(resp => {
if (resp && resp.success) {
data.valid = true;
data.tip = `Status: ${resp.status}`;
} else {
console.log(resp);
data.valid = false;
if (resp.error) {
data.tip = resp.error.detail ? resp.error.detail : JSON.stringify(resp.error);
}
}
});
}
}

@ -263,6 +263,83 @@ export class ClientServiceProxy {
return _observableOf<any>(<any>null);
}
/**
* @param input request
* @return success
*/
healthCheck(input: HealthCheckRequest) : Observable<any> {
let url_ = this.baseUrl + "/api/client/healthcheck";
url_ = url_.replace(/[?&]$/, "");
const content_ = JSON.stringify(input);
let options_ : any = {
body: content_,
observe: "response",
responseType: "blob",
headers: new HttpHeaders({
"Content-Type": "application/json",
"Accept": "application/json"
})
};
return this.http.request("post", url_, options_).pipe(_observableMergeMap((response_ : any) => {
return this.processHealthCheck(response_);
})).pipe(_observableCatch((response_: any) => {
if (response_ instanceof HttpResponseBase) {
try {
return this.processHealthCheck(<any>response_);
} catch (e) {
return <Observable<any>><any>_observableThrow(e);
}
} else
return <Observable<any>><any>_observableThrow(response_);
}));
}
protected processHealthCheck(response: HttpResponseBase): Observable<any> {
const status = response.status;
const responseBlob =
response instanceof HttpResponse ? response.body :
(<any>response).error instanceof Blob ? (<any>response).error : undefined;
let _headers: any = {}; if (response.headers) { for (let key of response.headers.keys()) { _headers[key] = response.headers.get(key); }}
if (status === 200) {
return blobToText(responseBlob).pipe(_observableMergeMap(_responseText => {
let result200: any = null;
let resultData200 = _responseText === "" ? null : JSON.parse(_responseText, this.jsonParseReviver);
result200 = resultData200 !== undefined ? resultData200 : <any>null;
return _observableOf(result200);
}));
} else if (status === 400) {
return blobToText(responseBlob).pipe(_observableMergeMap(_responseText => {
let result400: any = null;
let resultData400 = _responseText === "" ? null : JSON.parse(_responseText, this.jsonParseReviver);
result400 = resultData400 !== undefined ? resultData400 : <any>null;
return throwException("Bad Request", status, _responseText, _headers, result400);
}));
} else if (status === 401) {
return blobToText(responseBlob).pipe(_observableMergeMap(_responseText => {
let result401: any = null;
let resultData401 = _responseText === "" ? null : JSON.parse(_responseText, this.jsonParseReviver);
result401 = resultData401 !== undefined ? resultData401 : <any>null;
return throwException("Unauthorized", status, _responseText, _headers, result401);
}));
} else if (status === 500) {
return blobToText(responseBlob).pipe(_observableMergeMap(_responseText => {
let result500: any = null;
let resultData500 = _responseText === "" ? null : JSON.parse(_responseText, this.jsonParseReviver);
result500 = resultData500 !== undefined ? resultData500 : <any>null;
return throwException("Internal Server Error", status, _responseText, _headers, result500);
}));
} else if (status !== 200 && status !== 204) {
return blobToText(responseBlob).pipe(_observableMergeMap(_responseText => {
return throwException("An unexpected server error occurred.", status, _responseText, _headers);
}));
}
return _observableOf<any>(<any>null);
}
/**
* @param input request
* @return success
@ -512,6 +589,78 @@ export class RegistryServiceProxy {
return _observableOf<GetServiceHandlersResponse>(<any>null);
}
/**
* @return OK
*/
getNodes() : Observable<GetNodeListResponse> {
let url_ = this.baseUrl + "/api/registry/service/nodes";
url_ = url_.replace(/[?&]$/, "");
let options_ : any = {
observe: "response",
responseType: "blob",
headers: new HttpHeaders({
"Accept": "application/json"
})
};
return this.http.request("get", url_, options_).pipe(_observableMergeMap((response_ : any) => {
return this.processGetNodes(response_);
})).pipe(_observableCatch((response_: any) => {
if (response_ instanceof HttpResponseBase) {
try {
return this.processGetNodes(<any>response_);
} catch (e) {
return <Observable<GetNodeListResponse>><any>_observableThrow(e);
}
} else
return <Observable<GetNodeListResponse>><any>_observableThrow(response_);
}));
}
protected processGetNodes(response: HttpResponseBase): Observable<GetNodeListResponse> {
const status = response.status;
const responseBlob =
response instanceof HttpResponse ? response.body :
(<any>response).error instanceof Blob ? (<any>response).error : undefined;
let _headers: any = {}; if (response.headers) { for (let key of response.headers.keys()) { _headers[key] = response.headers.get(key); }}
if (status === 200) {
return blobToText(responseBlob).pipe(_observableMergeMap(_responseText => {
let result200: any = null;
let resultData200 = _responseText === "" ? null : JSON.parse(_responseText, this.jsonParseReviver);
result200 = GetNodeListResponse.fromJS(resultData200);
return _observableOf(result200);
}));
} else if (status === 400) {
return blobToText(responseBlob).pipe(_observableMergeMap(_responseText => {
let result400: any = null;
let resultData400 = _responseText === "" ? null : JSON.parse(_responseText, this.jsonParseReviver);
result400 = resultData400 !== undefined ? resultData400 : <any>null;
return throwException("Bad Request", status, _responseText, _headers, result400);
}));
} else if (status === 401) {
return blobToText(responseBlob).pipe(_observableMergeMap(_responseText => {
let result401: any = null;
let resultData401 = _responseText === "" ? null : JSON.parse(_responseText, this.jsonParseReviver);
result401 = resultData401 !== undefined ? resultData401 : <any>null;
return throwException("Unauthorized", status, _responseText, _headers, result401);
}));
} else if (status === 500) {
return blobToText(responseBlob).pipe(_observableMergeMap(_responseText => {
let result500: any = null;
let resultData500 = _responseText === "" ? null : JSON.parse(_responseText, this.jsonParseReviver);
result500 = resultData500 !== undefined ? resultData500 : <any>null;
return throwException("Internal Server Error", status, _responseText, _headers, result500);
}));
} else if (status !== 200 && status !== 204) {
return blobToText(responseBlob).pipe(_observableMergeMap(_responseText => {
return throwException("An unexpected server error occurred.", status, _responseText, _headers);
}));
}
return _observableOf<GetNodeListResponse>(<any>null);
}
/**
* @param name service name
* @param version (optional) service version
@ -976,6 +1125,54 @@ export interface ICallRequest {
version?: string | undefined;
}
export class HealthCheckRequest implements IHealthCheckRequest {
address!: string;
service!: string;
timeout?: number | undefined;
version!: string;
constructor(data?: IHealthCheckRequest) {
if (data) {
for (var property in data) {
if (data.hasOwnProperty(property))
(<any>this)[property] = (<any>data)[property];
}
}
}
init(_data?: any) {
if (_data) {
this.address = _data["address"];
this.service = _data["service"];
this.timeout = _data["timeout"];
this.version = _data["version"];
}
}
static fromJS(data: any): HealthCheckRequest {
data = typeof data === 'object' ? data : {};
let result = new HealthCheckRequest();
result.init(data);
return result;
}
toJSON(data?: any) {
data = typeof data === 'object' ? data : {};
data["address"] = this.address;
data["service"] = this.service;
data["timeout"] = this.timeout;
data["version"] = this.version;
return data;
}
}
export interface IHealthCheckRequest {
address: string;
service: string;
timeout?: number | undefined;
version: string;
}
export class PublishRequest implements IPublishRequest {
message!: string;
topic!: string;
@ -1016,6 +1213,50 @@ export interface IPublishRequest {
topic: string;
}
export class GetNodeListResponse implements IGetNodeListResponse {
services?: RegistryServiceNodes[] | undefined;
constructor(data?: IGetNodeListResponse) {
if (data) {
for (var property in data) {
if (data.hasOwnProperty(property))
(<any>this)[property] = (<any>data)[property];
}
}
}
init(_data?: any) {
if (_data) {
if (Array.isArray(_data["services"])) {
this.services = [] as any;
for (let item of _data["services"])
this.services!.push(RegistryServiceNodes.fromJS(item));
}
}
}
static fromJS(data: any): GetNodeListResponse {
data = typeof data === 'object' ? data : {};
let result = new GetNodeListResponse();
result.init(data);
return result;
}
toJSON(data?: any) {
data = typeof data === 'object' ? data : {};
if (Array.isArray(this.services)) {
data["services"] = [];
for (let item of this.services)
data["services"].push(item.toJSON());
}
return data;
}
}
export interface IGetNodeListResponse {
services?: RegistryServiceNodes[] | undefined;
}
export class GetServiceDetailResponse implements IGetServiceDetailResponse {
services?: RegistryService[] | undefined;
@ -1200,6 +1441,7 @@ export class RegistryEndpoint implements IRegistryEndpoint {
name!: string;
request!: RegistryValue;
response?: RegistryValue | undefined;
stream?: boolean | undefined;
constructor(data?: IRegistryEndpoint) {
if (data) {
@ -1225,6 +1467,7 @@ export class RegistryEndpoint implements IRegistryEndpoint {
this.name = _data["name"];
this.request = _data["request"] ? RegistryValue.fromJS(_data["request"]) : new RegistryValue();
this.response = _data["response"] ? RegistryValue.fromJS(_data["response"]) : <any>undefined;
this.stream = _data["stream"];
}
}
@ -1247,6 +1490,7 @@ export class RegistryEndpoint implements IRegistryEndpoint {
data["name"] = this.name;
data["request"] = this.request ? this.request.toJSON() : <any>undefined;
data["response"] = this.response ? this.response.toJSON() : <any>undefined;
data["stream"] = this.stream;
return data;
}
}
@ -1256,6 +1500,7 @@ export interface IRegistryEndpoint {
name: string;
request: RegistryValue;
response?: RegistryValue | undefined;
stream?: boolean | undefined;
}
export class RegistryNode implements IRegistryNode {
@ -1314,6 +1559,66 @@ export interface IRegistryNode {
metadata?: { [key: string]: string; } | undefined;
}
export class RegistryNodeDetail implements IRegistryNodeDetail {
address!: string;
id!: string;
metadata?: { [key: string]: string; } | undefined;
version!: string;
constructor(data?: IRegistryNodeDetail) {
if (data) {
for (var property in data) {
if (data.hasOwnProperty(property))
(<any>this)[property] = (<any>data)[property];
}
}
}
init(_data?: any) {
if (_data) {
this.address = _data["address"];
this.id = _data["id"];
if (_data["metadata"]) {
this.metadata = {} as any;
for (let key in _data["metadata"]) {
if (_data["metadata"].hasOwnProperty(key))
(<any>this.metadata)![key] = _data["metadata"][key];
}
}
this.version = _data["version"];
}
}
static fromJS(data: any): RegistryNodeDetail {
data = typeof data === 'object' ? data : {};
let result = new RegistryNodeDetail();
result.init(data);
return result;
}
toJSON(data?: any) {
data = typeof data === 'object' ? data : {};
data["address"] = this.address;
data["id"] = this.id;
if (this.metadata) {
data["metadata"] = {};
for (let key in this.metadata) {
if (this.metadata.hasOwnProperty(key))
(<any>data["metadata"])[key] = this.metadata[key];
}
}
data["version"] = this.version;
return data;
}
}
export interface IRegistryNodeDetail {
address: string;
id: string;
metadata?: { [key: string]: string; } | undefined;
version: string;
}
export class RegistryService implements IRegistryService {
handlers?: RegistryEndpoint[] | undefined;
metadata?: { [key: string]: string; } | undefined;
@ -1406,6 +1711,54 @@ export interface IRegistryService {
version: string;
}
export class RegistryServiceNodes implements IRegistryServiceNodes {
name?: string | undefined;
nodes?: RegistryNodeDetail[] | undefined;
constructor(data?: IRegistryServiceNodes) {
if (data) {
for (var property in data) {
if (data.hasOwnProperty(property))
(<any>this)[property] = (<any>data)[property];
}
}
}
init(_data?: any) {
if (_data) {
this.name = _data["name"];
if (Array.isArray(_data["nodes"])) {
this.nodes = [] as any;
for (let item of _data["nodes"])
this.nodes!.push(RegistryNodeDetail.fromJS(item));
}
}
}
static fromJS(data: any): RegistryServiceNodes {
data = typeof data === 'object' ? data : {};
let result = new RegistryServiceNodes();
result.init(data);
return result;
}
toJSON(data?: any) {
data = typeof data === 'object' ? data : {};
data["name"] = this.name;
if (Array.isArray(this.nodes)) {
data["nodes"] = [];
for (let item of this.nodes)
data["nodes"].push(item.toJSON());
}
return data;
}
}
export interface IRegistryServiceNodes {
name?: string | undefined;
nodes?: RegistryNodeDetail[] | undefined;
}
export class RegistryServiceSummary implements IRegistryServiceSummary {
name!: string;
versions?: string[] | undefined;

@ -54,6 +54,9 @@ import {
WeiboCircleOutline,
ClearOutline,
InfoCircleOutline,
ApartmentOutline,
CheckCircleTwoTone,
CloseCircleTwoTone,
} from '@ant-design/icons-angular/icons';
export const ICONS_AUTO = [
@ -107,4 +110,7 @@ export const ICONS_AUTO = [
WeiboCircleOutline,
ClearOutline,
InfoCircleOutline,
ApartmentOutline,
CheckCircleTwoTone,
CloseCircleTwoTone,
];

@ -12,3 +12,10 @@ type publishRequest struct {
Topic string `json:"topic" binding:"required"`
Message string `json:"message" binding:"required"`
}
type healthCheckRequest struct {
Service string `json:"service" binding:"required"`
Version string `json:"version" binding:"required"`
Address string `json:"address" binding:"required"`
Timeout int64 `json:"timeout"`
}

@ -13,6 +13,7 @@ import (
"github.com/gin-gonic/gin/render"
"github.com/xpunch/go-micro-dashboard/handler/route"
"go-micro.dev/v4/client"
debug "go-micro.dev/v4/debug/proto"
"go-micro.dev/v4/errors"
"go-micro.dev/v4/registry"
"go-micro.dev/v4/selector"
@ -33,7 +34,8 @@ func NewRouteRegistrar(client client.Client, registry registry.Registry) route.R
func (s *service) RegisterRoute(router gin.IRoutes) {
router.Use(route.AuthRequired()).
POST("/api/client/call", s.Call).
POST("/api/client/publish", s.Publish)
POST("/api/client/publish", s.Publish).
POST("/api/client/healthcheck", s.HealthCheck)
}
// @Security ApiKeyAuth
@ -99,6 +101,62 @@ func (s *service) Call(ctx *gin.Context) {
ctx.JSON(200, resp)
}
// @Security ApiKeyAuth
// @Tags Client
// @ID client_healthCheck
// @Param input body healthCheckRequest true "request"
// @Success 200 {object} object "success"
// @Failure 400 {object} string
// @Failure 401 {object} string
// @Failure 500 {object} string
// @Router /api/client/healthcheck [post]
func (s *service) HealthCheck(ctx *gin.Context) {
var req healthCheckRequest
if err := ctx.ShouldBindJSON(&req); nil != err {
ctx.Render(400, render.String{Format: err.Error()})
return
}
services, err := s.registry.GetService(req.Service)
if err != nil {
ctx.JSON(200, gin.H{"success": false, "error": err.Error()})
return
}
var c client.Client
for _, srv := range services {
if len(req.Version) > 0 && req.Version != srv.Version {
continue
}
for _, n := range srv.Nodes {
if req.Address == n.Address {
c = s.getClient(n.Metadata["server"])
break
}
}
}
if c == nil {
ctx.JSON(200, gin.H{"success": false, "error": "service node not found"})
return
}
callOpts := []client.CallOption{
client.WithAddress(req.Address),
client.WithSelectOption(selector.WithFilter(selector.FilterVersion(req.Version))),
}
if req.Timeout > 0 {
callOpts = append(callOpts, client.WithRequestTimeout(time.Duration(req.Timeout)*time.Second))
}
debugService := debug.NewDebugService(req.Service, c)
reply, err := debugService.Health(ctx, &debug.HealthRequest{}, callOpts...)
if 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, gin.H{"success": true, "status": reply.Status})
}
// @Security ApiKeyAuth
// @Tags Client
// @ID client_publish

@ -24,6 +24,7 @@ type registryEndpoint struct {
Name string `json:"name" binding:"required"`
Request registryValue `json:"request" binding:"required"`
Response registryValue `json:"response"`
Stream bool `json:"stream,omitempty"`
Metadata map[string]string `json:"metadata,omitempty"`
}
@ -51,6 +52,22 @@ type getServiceSubscribersResponse struct {
Subscribers []registryEndpoint `json:"subscribers"`
}
type registryNodeDetail struct {
Id string `json:"id" binding:"required"`
Version string `json:"version" binding:"required"`
Address string `json:"address" binding:"required"`
Metadata map[string]string `json:"metadata,omitempty"`
}
type registryServiceNodes struct {
Name string `json:"name"`
Nodes []registryNodeDetail `json:"nodes"`
}
type getNodeListResponse struct {
Services []registryServiceNodes `json:"services"`
}
func convertRegistryValue(v *registry.Value) registryValue {
if v == nil {
return registryValue{}

@ -22,7 +22,8 @@ func (s service) RegisterRoute(router gin.IRoutes) {
GET("/api/registry/services", s.GetServices).
GET("/api/registry/service", s.GetServiceDetail).
GET("/api/registry/service/handlers", s.GetServiceHandlers).
GET("/api/registry/service/subscribers", s.GetServiceSubscribers)
GET("/api/registry/service/subscribers", s.GetServiceSubscribers).
GET("/api/registry/service/nodes", s.GetServiceNodes)
}
// @Security ApiKeyAuth
@ -159,6 +160,7 @@ func (s *service) GetServiceHandlers(ctx *gin.Context) {
handlers = append(handlers, registryEndpoint{
Name: e.Name,
Request: convertRegistryValue(e.Request),
Stream: isStream(e),
})
}
resp.Handlers = handlers
@ -210,6 +212,63 @@ func (s *service) GetServiceSubscribers(ctx *gin.Context) {
ctx.JSON(200, resp)
}
// @Security ApiKeyAuth
// @Tags Registry
// @ID registry_getNodes
// @Success 200 {object} getNodeListResponse
// @Failure 400 {object} string
// @Failure 401 {object} string
// @Failure 500 {object} string
// @Router /api/registry/service/nodes [get]
func (s *service) GetServiceNodes(ctx *gin.Context) {
serviceNames, err := s.registry.ListServices()
if err != nil {
ctx.Render(500, render.String{Format: err.Error()})
return
}
sCache := make(map[string]map[string]registryNodeDetail)
for _, sn := range serviceNames {
if _, ok := sCache[sn.Name]; ok {
continue
}
sv, err := s.registry.GetService(sn.Name)
if err != nil {
ctx.Render(500, render.String{Format: err.Error()})
return
}
nCache := make(map[string]registryNodeDetail)
for _, v := range sv {
for _, n := range v.Nodes {
if _, ok := nCache[n.Id]; ok {
continue
}
nCache[n.Id] = registryNodeDetail{
Id: n.Id,
Version: v.Version,
Address: n.Address,
Metadata: n.Metadata,
}
}
}
sCache[sn.Name] = nCache
}
resp := getNodeListResponse{Services: make([]registryServiceNodes, 0)}
for k, v := range sCache {
nodes := make([]registryNodeDetail, 0, len(v))
for _, n := range v {
nodes = append(nodes, n)
}
sort.Slice(nodes, func(i, j int) bool {
return nodes[i].Id < nodes[j].Id
})
resp.Services = append(resp.Services, registryServiceNodes{Name: k, Nodes: nodes})
}
sort.Slice(resp.Services, func(i, j int) bool {
return resp.Services[i].Name < resp.Services[j].Name
})
ctx.JSON(200, resp)
}
func isSubscriber(ep *registry.Endpoint) bool {
if ep == nil || len(ep.Metadata) == 0 {
return false
@ -219,3 +278,13 @@ func isSubscriber(ep *registry.Endpoint) bool {
}
return false
}
func isStream(ep *registry.Endpoint) bool {
if ep == nil || len(ep.Metadata) == 0 {
return false
}
if s, ok := ep.Metadata["stream"]; ok && s == "true" {
return true
}
return false
}

@ -10,7 +10,7 @@ import (
)
// @title Go Micro Dashboard
// @version 1.2.0
// @version 1.3.0
// @description go micro dashboard restful-api
// @termsOfService http://swagger.io/terms/
// @BasePath /

@ -1,5 +1,5 @@
// Code generated by fileb0x at "2021-12-07 10:11:10.4808383 +0800 CST m=+0.057013201" from config file "b0x.yaml" DO NOT EDIT.
// modification hash(fecae112933101acf070cd0740e400a2.8be3f833d63e3c844663716446e13a42)
// Code generated by fileb0x at "2021-12-16 15:53:47.4939586 +0800 CST m=+0.083849201" from config file "b0x.yaml" DO NOT EDIT.
// modification hash(cde2b36716bcc13b0314daaf715fd459.8be3f833d63e3c844663716446e13a42)
package web

@ -1,5 +1,5 @@
// Code generaTed by fileb0x at "2021-12-07 10:11:10.6402683 +0800 CST m=+0.216443201" from config file "b0x.yaml" DO NOT EDIT.
// modified(2021-12-07 10:11:05.7530359 +0800 CST)
// Code generaTed by fileb0x at "2021-12-16 15:53:47.5464758 +0800 CST m=+0.136366401" from config file "b0x.yaml" DO NOT EDIT.
// modified(2021-12-16 15:47:21.1187489 +0800 CST)
// original path: frontend\dist\449.ea3505f8c3a78bc5ec51.js
package web

File diff suppressed because one or more lines are too long

@ -1,5 +1,5 @@
// Code generaTed by fileb0x at "2021-12-07 10:11:10.6541906 +0800 CST m=+0.230365501" from config file "b0x.yaml" DO NOT EDIT.
// modified(2021-12-07 10:11:05.7530359 +0800 CST)
// Code generaTed by fileb0x at "2021-12-16 15:53:47.6739431 +0800 CST m=+0.263833701" from config file "b0x.yaml" DO NOT EDIT.
// modified(2021-12-16 15:47:21.1187489 +0800 CST)
// original path: frontend\dist\polyfills.d3127c390f57a23419e1.js
package web

@ -1,5 +1,5 @@
// Code generaTed by fileb0x at "2021-12-07 10:11:10.5996812 +0800 CST m=+0.175856101" from config file "b0x.yaml" DO NOT EDIT.
// modified(2021-12-07 10:11:05.7530359 +0800 CST)
// Code generaTed by fileb0x at "2021-12-16 15:53:47.678858 +0800 CST m=+0.268748601" from config file "b0x.yaml" DO NOT EDIT.
// modified(2021-12-16 15:47:21.1187489 +0800 CST)
// original path: frontend\dist\runtime.0edec77bcc0e6f764d02.js
package web

@ -1,5 +1,5 @@
// Code generaTed by fileb0x at "2021-12-07 10:11:10.6017344 +0800 CST m=+0.177909301" from config file "b0x.yaml" DO NOT EDIT.
// modified(2021-12-07 10:11:05.7530359 +0800 CST)
// Code generaTed by fileb0x at "2021-12-16 15:53:47.5088999 +0800 CST m=+0.098790501" from config file "b0x.yaml" DO NOT EDIT.
// modified(2021-12-16 15:47:21.1408101 +0800 CST)
// original path: frontend\dist\styles.40a72a43f1959e5b9ccc.css
package web

Loading…
Cancel
Save