Merge pull request #33 from hunterlong/dev

TCP check - timeouts
pull/44/merge
Hunter Long 2018-07-18 16:09:53 -07:00 committed by GitHub
commit 60784234df
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
15 changed files with 138 additions and 38 deletions

View File

@ -33,15 +33,10 @@ func CheckQueue(s *types.Service) {
return return
default: default:
ServiceCheck(s) ServiceCheck(s)
if s.Interval < 1 {
s.Interval = 1
} }
msg := fmt.Sprintf("Service: %v | Online: %v | Latency: %0.0fms", s.Name, s.Online, (s.Latency * 1000))
utils.Log(1, msg)
time.Sleep(time.Duration(s.Interval) * time.Second) time.Sleep(time.Duration(s.Interval) * time.Second)
} }
} }
}
func DNSCheck(s *types.Service) (float64, error) { func DNSCheck(s *types.Service) (float64, error) {
t1 := time.Now() t1 := time.Now()
@ -58,7 +53,39 @@ func DNSCheck(s *types.Service) (float64, error) {
return subTime, err return subTime, err
} }
func ServiceTCPCheck(s *types.Service) *types.Service {
t1 := time.Now()
domain := fmt.Sprintf("%v", s.Domain)
if s.Port != 0 {
domain = fmt.Sprintf("%v:%v", s.Domain, s.Port)
}
conn, err := net.DialTimeout("tcp", domain, time.Duration(s.Timeout)*time.Second)
if err != nil {
RecordFailure(s, fmt.Sprintf("TCP Dial Error %v", err))
return s
}
if err := conn.Close(); err != nil {
RecordFailure(s, fmt.Sprintf("TCP Socket Close Error %v", err))
return s
}
t2 := time.Now()
s.Latency = t2.Sub(t1).Seconds()
s.LastResponse = ""
RecordSuccess(s)
return s
}
func ServiceCheck(s *types.Service) *types.Service { func ServiceCheck(s *types.Service) *types.Service {
switch s.Type {
case "http":
ServiceHTTPCheck(s)
case "tcp":
ServiceTCPCheck(s)
}
return s
}
func ServiceHTTPCheck(s *types.Service) *types.Service {
dnsLookup, err := DNSCheck(s) dnsLookup, err := DNSCheck(s)
if err != nil { if err != nil {
RecordFailure(s, fmt.Sprintf("Could not get IP address for domain %v, %v", s.Domain, err)) RecordFailure(s, fmt.Sprintf("Could not get IP address for domain %v, %v", s.Domain, err))
@ -66,8 +93,9 @@ func ServiceCheck(s *types.Service) *types.Service {
} }
s.DnsLookup = dnsLookup s.DnsLookup = dnsLookup
t1 := time.Now() t1 := time.Now()
timeout := time.Duration(s.Timeout)
client := http.Client{ client := http.Client{
Timeout: 30 * time.Second, Timeout: timeout * time.Second,
} }
var response *http.Response var response *http.Response
@ -113,7 +141,7 @@ func ServiceCheck(s *types.Service) *types.Service {
s.LastResponse = string(contents) s.LastResponse = string(contents)
s.LastStatusCode = response.StatusCode s.LastStatusCode = response.StatusCode
s.Online = true s.Online = true
RecordSuccess(s, response) RecordSuccess(s)
return s return s
} }
@ -121,12 +149,13 @@ type HitData struct {
Latency float64 Latency float64
} }
func RecordSuccess(s *types.Service, response *http.Response) { func RecordSuccess(s *types.Service) {
s.Online = true s.Online = true
s.LastOnline = time.Now() s.LastOnline = time.Now()
data := HitData{ data := HitData{
Latency: s.Latency, Latency: s.Latency,
} }
utils.Log(1, fmt.Sprintf("Service %v Successful: %0.2f ms", s.Name, data.Latency*1000))
CreateServiceHit(s, data) CreateServiceHit(s, data)
OnSuccess(s) OnSuccess(s)
} }

View File

@ -25,27 +25,27 @@ func LoadSampleData() error {
Domain: "https://google.com", Domain: "https://google.com",
ExpectedStatus: 200, ExpectedStatus: 200,
Interval: 10, Interval: 10,
Port: 0,
Type: "http", Type: "http",
Method: "GET", Method: "GET",
Timeout: 10,
} }
s2 := &types.Service{ s2 := &types.Service{
Name: "Statup Github", Name: "Statup Github",
Domain: "https://github.com/hunterlong/statup", Domain: "https://github.com/hunterlong/statup",
ExpectedStatus: 200, ExpectedStatus: 200,
Interval: 30, Interval: 30,
Port: 0,
Type: "http", Type: "http",
Method: "GET", Method: "GET",
Timeout: 20,
} }
s3 := &types.Service{ s3 := &types.Service{
Name: "JSON Users Test", Name: "JSON Users Test",
Domain: "https://jsonplaceholder.typicode.com/users", Domain: "https://jsonplaceholder.typicode.com/users",
ExpectedStatus: 200, ExpectedStatus: 200,
Interval: 60, Interval: 60,
Port: 443,
Type: "http", Type: "http",
Method: "GET", Method: "GET",
Timeout: 30,
} }
s4 := &types.Service{ s4 := &types.Service{
Name: "JSON API Tester", Name: "JSON API Tester",
@ -56,6 +56,15 @@ func LoadSampleData() error {
Type: "http", Type: "http",
Method: "POST", Method: "POST",
PostData: `{ "title": "statup", "body": "bar", "userId": 19999 }`, PostData: `{ "title": "statup", "body": "bar", "userId": 19999 }`,
Timeout: 30,
}
s5 := &types.Service{
Name: "Postgres TCP Check",
Domain: "0.0.0.0",
Interval: 20,
Type: "tcp",
Port: 5432,
Timeout: 120,
} }
id, err := CreateService(s1) id, err := CreateService(s1)
if err != nil { if err != nil {
@ -73,6 +82,10 @@ func LoadSampleData() error {
if err != nil { if err != nil {
utils.Log(3, fmt.Sprintf("Error creating Service %v: %v", id, err)) utils.Log(3, fmt.Sprintf("Error creating Service %v: %v", id, err))
} }
id, err = CreateService(s5)
if err != nil {
utils.Log(3, fmt.Sprintf("Error creating TCP Service %v: %v", id, err))
}
//checkin := &Checkin{ //checkin := &Checkin{
// Service: s2.Id, // Service: s2.Id,

View File

@ -38,6 +38,7 @@ func CreateServiceHandler(w http.ResponseWriter, r *http.Request) {
status, _ := strconv.Atoi(r.PostForm.Get("expected_status")) status, _ := strconv.Atoi(r.PostForm.Get("expected_status"))
interval, _ := strconv.Atoi(r.PostForm.Get("interval")) interval, _ := strconv.Atoi(r.PostForm.Get("interval"))
port, _ := strconv.Atoi(r.PostForm.Get("port")) port, _ := strconv.Atoi(r.PostForm.Get("port"))
timeout, _ := strconv.Atoi(r.PostForm.Get("timeout"))
checkType := r.PostForm.Get("check_type") checkType := r.PostForm.Get("check_type")
postData := r.PostForm.Get("post_data") postData := r.PostForm.Get("post_data")
@ -51,6 +52,7 @@ func CreateServiceHandler(w http.ResponseWriter, r *http.Request) {
Type: checkType, Type: checkType,
Port: port, Port: port,
PostData: postData, PostData: postData,
Timeout: timeout,
} }
_, err := core.CreateService(service) _, err := core.CreateService(service)
if err != nil { if err != nil {
@ -78,9 +80,6 @@ func ServicesDeleteHandler(w http.ResponseWriter, r *http.Request) {
func ServicesViewHandler(w http.ResponseWriter, r *http.Request) { func ServicesViewHandler(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r) vars := mux.Vars(r)
serv := core.SelectService(utils.StringInt(vars["id"])) serv := core.SelectService(utils.StringInt(vars["id"]))
fmt.Println(serv.ToService())
ExecuteResponse(w, r, "service.html", serv) ExecuteResponse(w, r, "service.html", serv)
} }
@ -116,6 +115,7 @@ func ServicesUpdateHandler(w http.ResponseWriter, r *http.Request) {
status, _ := strconv.Atoi(r.PostForm.Get("expected_status")) status, _ := strconv.Atoi(r.PostForm.Get("expected_status"))
interval, _ := strconv.Atoi(r.PostForm.Get("interval")) interval, _ := strconv.Atoi(r.PostForm.Get("interval"))
port, _ := strconv.Atoi(r.PostForm.Get("port")) port, _ := strconv.Atoi(r.PostForm.Get("port"))
timeout, _ := strconv.Atoi(r.PostForm.Get("timeout"))
checkType := r.PostForm.Get("check_type") checkType := r.PostForm.Get("check_type")
postData := r.PostForm.Get("post_data") postData := r.PostForm.Get("post_data")
serviceUpdate := &types.Service{ serviceUpdate := &types.Service{
@ -129,6 +129,7 @@ func ServicesUpdateHandler(w http.ResponseWriter, r *http.Request) {
Type: checkType, Type: checkType,
Port: port, Port: port,
PostData: postData, PostData: postData,
Timeout: timeout,
} }
service = core.UpdateService(serviceUpdate) service = core.UpdateService(serviceUpdate)
ExecuteResponse(w, r, "service.html", service) ExecuteResponse(w, r, "service.html", service)

View File

@ -249,7 +249,7 @@ func RunSelectAllMysqlServices(t *testing.T) {
var err error var err error
services, err := core.SelectAllServices() services, err := core.SelectAllServices()
assert.Nil(t, err) assert.Nil(t, err)
assert.Equal(t, 4, len(services)) assert.Equal(t, 5, len(services))
} }
func RunSelectAllMysqlCommunications(t *testing.T) { func RunSelectAllMysqlCommunications(t *testing.T) {
@ -320,7 +320,7 @@ func RunSelectAllServices(t *testing.T) {
var err error var err error
services, err := core.SelectAllServices() services, err := core.SelectAllServices()
assert.Nil(t, err) assert.Nil(t, err)
assert.Equal(t, 4, len(services)) assert.Equal(t, 5, len(services))
} }
func RunOneService_Check(t *testing.T) { func RunOneService_Check(t *testing.T) {
@ -339,10 +339,11 @@ func RunService_Create(t *testing.T) {
Port: 0, Port: 0,
Type: "http", Type: "http",
Method: "GET", Method: "GET",
Timeout: 30,
} }
id, err := core.CreateService(service) id, err := core.CreateService(service)
assert.Nil(t, err) assert.Nil(t, err)
assert.Equal(t, int64(5), id) assert.Equal(t, int64(6), id)
t.Log(service) t.Log(service)
} }
@ -379,10 +380,11 @@ func RunBadService_Create(t *testing.T) {
Port: 0, Port: 0,
Type: "http", Type: "http",
Method: "GET", Method: "GET",
Timeout: 30,
} }
id, err := core.CreateService(service) id, err := core.CreateService(service)
assert.Nil(t, err) assert.Nil(t, err)
assert.Equal(t, int64(6), id) assert.Equal(t, int64(7), id)
} }
func RunBadService_Check(t *testing.T) { func RunBadService_Check(t *testing.T) {
@ -405,7 +407,12 @@ func RunCreateService_Hits(t *testing.T) {
assert.NotNil(t, services) assert.NotNil(t, services)
for i := 0; i <= 10; i++ { for i := 0; i <= 10; i++ {
for _, s := range services { for _, s := range services {
service := core.ServiceCheck(s.ToService()) var service *types.Service
if s.ToService().Type == "http" {
service = core.ServiceHTTPCheck(s.ToService())
} else {
service = core.ServiceTCPCheck(s.ToService())
}
assert.NotNil(t, service) assert.NotNil(t, service)
} }
} }
@ -459,7 +466,7 @@ func RunPrometheusHandler(t *testing.T) {
rr := httptest.NewRecorder() rr := httptest.NewRecorder()
route.ServeHTTP(rr, req) route.ServeHTTP(rr, req)
t.Log(rr.Body.String()) t.Log(rr.Body.String())
assert.True(t, strings.Contains(rr.Body.String(), "statup_total_services 5")) assert.True(t, strings.Contains(rr.Body.String(), "statup_total_services 6"))
} }
func RunFailingPrometheusHandler(t *testing.T) { func RunFailingPrometheusHandler(t *testing.T) {

View File

@ -66,6 +66,12 @@ func init() {
Title: "Outgoing Email Address", Title: "Outgoing Email Address",
Placeholder: "Insert your Outgoing Email Address", Placeholder: "Insert your Outgoing Email Address",
DbField: "Var1", DbField: "Var1",
}, {
Id: 1,
Type: "email",
Title: "Send Alerts To",
Placeholder: "Email Address",
DbField: "Var2",
}}, }},
}} }}
@ -103,7 +109,7 @@ func (u *Email) Init() error {
func (u *Email) Test() error { func (u *Email) Test() error {
if u.Enabled { if u.Enabled {
email := &EmailOutgoing{ email := &EmailOutgoing{
To: "info@socialeck.com", To: emailer.Var2,
Subject: "Test Email", Subject: "Test Email",
Template: "message.html", Template: "message.html",
Data: nil, Data: nil,
@ -159,13 +165,11 @@ func (u *Email) Run() error {
// ON SERVICE FAILURE, DO YOUR OWN FUNCTIONS // ON SERVICE FAILURE, DO YOUR OWN FUNCTIONS
func (u *Email) OnFailure(s *types.Service) error { func (u *Email) OnFailure(s *types.Service) error {
if u.Enabled { if u.Enabled {
msg := emailMessage{ msg := emailMessage{
Service: s, Service: s,
} }
email := &EmailOutgoing{ email := &EmailOutgoing{
To: "info@socialeck.com", To: emailer.Var2,
Subject: fmt.Sprintf("Service %v is Failing", s.Name), Subject: fmt.Sprintf("Service %v is Failing", s.Name),
Template: "failure.html", Template: "failure.html",
Data: msg, Data: msg,

View File

@ -10,6 +10,26 @@ $('form').submit(function() {
$(this).find("button[type='submit']").prop('disabled',true); $(this).find("button[type='submit']").prop('disabled',true);
}); });
$('select#service_type').on('change', function() {
var selected = $('#service_type option:selected').val();
if (selected == "tcp") {
$("#service_port").parent().parent().removeClass("d-none");
$("#service_check_type").parent().parent().addClass("d-none");
$("#post_data").parent().parent().addClass("d-none");
$("#service_response").parent().parent().addClass("d-none");
$("#service_response_code").parent().parent().addClass("d-none");
} else {
$("#post_data").parent().parent().removeClass("d-none");
$("#service_response").parent().parent().removeClass("d-none");
$("#service_response_code").parent().parent().removeClass("d-none");
$("#service_check_type").parent().parent().removeClass("d-none");
$("#service_port").parent().parent().addClass("d-none");
}
});
$(".confirm-btn").on('click', function() { $(".confirm-btn").on('click', function() {
var r = confirm("Are you sure you want to delete?"); var r = confirm("Are you sure you want to delete?");

View File

@ -34,8 +34,9 @@ CREATE TABLE services (
expected_status INT(6), expected_status INT(6),
check_interval int(11), check_interval int(11),
post_data text, post_data text,
order_id integer, order_id integer default 0,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP, created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
timeout INT(6) DEFAULT 30,
INDEX (id) INDEX (id)
); );
CREATE TABLE hits ( CREATE TABLE hits (

View File

@ -1,3 +1,6 @@
=========================================== 1531891670
ALTER TABLE services ALTER COLUMN order_id SET DEFAULT 0;
ALTER TABLE services ADD COLUMN timeout integer DEFAULT 30;
=========================================== 1530841150 =========================================== 1530841150
ALTER TABLE core ADD COLUMN use_cdn BOOL NOT NULL DEFAULT '0'; ALTER TABLE core ADD COLUMN use_cdn BOOL NOT NULL DEFAULT '0';
=========================================== 1 =========================================== 1

View File

@ -34,7 +34,8 @@ CREATE TABLE services (
expected_status integer, expected_status integer,
check_interval integer, check_interval integer,
post_data text, post_data text,
order_id integer, order_id integer default 0,
timeout integer default 30,
created_at TIMESTAMP created_at TIMESTAMP
); );

View File

@ -1,3 +1,6 @@
=========================================== 1531891670
ALTER TABLE services ALTER COLUMN order_id SET DEFAULT 0;
ALTER TABLE services ADD COLUMN timeout integer DEFAULT 30;
=========================================== 1530841150 =========================================== 1530841150
ALTER TABLE core ADD COLUMN use_cdn bool DEFAULT FALSE; ALTER TABLE core ADD COLUMN use_cdn bool DEFAULT FALSE;
=========================================== 1 =========================================== 1

View File

@ -35,7 +35,8 @@ CREATE TABLE services (
expected_status integer, expected_status integer,
check_interval integer, check_interval integer,
post_data text, post_data text,
order_id integer, order_id integer default 0,
timeout integer default 30,
created_at DATETIME created_at DATETIME
); );

View File

@ -1,3 +1,6 @@
=========================================== 1531891670
ALTER TABLE services ALTER COLUMN order_id SET DEFAULT 0;
ALTER TABLE services ADD COLUMN timeout integer DEFAULT 30;
=========================================== 1530841150 =========================================== 1530841150
ALTER TABLE core ADD COLUMN use_cdn bool DEFAULT FALSE; ALTER TABLE core ADD COLUMN use_cdn bool DEFAULT FALSE;
=========================================== 1 =========================================== 1

View File

@ -104,7 +104,7 @@
<input type="text" name="domain" class="form-control" id="service_url" value="{{$s.Domain}}" placeholder="https://google.com" required> <input type="text" name="domain" class="form-control" id="service_url" value="{{$s.Domain}}" placeholder="https://google.com" required>
</div> </div>
</div> </div>
<div class="form-group row"> <div class="form-group row{{if eq $s.Type "tcp"}} d-none{{end}}">
<label for="service_check_type" class="col-sm-4 col-form-label">Service Check Type</label> <label for="service_check_type" class="col-sm-4 col-form-label">Service Check Type</label>
<div class="col-sm-8"> <div class="col-sm-8">
<select name="method" class="form-control" id="service_check_type" value="{{$s.Method}}"> <select name="method" class="form-control" id="service_check_type" value="{{$s.Method}}">
@ -113,25 +113,25 @@
</select> </select>
</div> </div>
</div> </div>
<div class="form-group row"> <div class="form-group row{{if eq $s.Type "tcp"}} d-none{{end}}">
<label for="post_data" class="col-sm-4 col-form-label">Post Data (JSON)</label> <label for="post_data" class="col-sm-4 col-form-label">Optional Post Data (JSON)</label>
<div class="col-sm-8"> <div class="col-sm-8">
<textarea name="post_data" class="form-control" id="post_data" rows="3">{{$s.PostData}}</textarea> <textarea name="post_data" class="form-control" id="post_data" rows="3">{{$s.PostData}}</textarea>
</div> </div>
</div> </div>
<div class="form-group row"> <div class="form-group row{{if eq $s.Type "tcp"}} d-none{{end}}">
<label for="service_response" class="col-sm-4 col-form-label">Expected Response (Regex)</label> <label for="service_response" class="col-sm-4 col-form-label">Expected Response (Regex)</label>
<div class="col-sm-8"> <div class="col-sm-8">
<textarea name="expected" class="form-control" id="service_response" rows="3">{{$s.Expected}}</textarea> <textarea name="expected" class="form-control" id="service_response" rows="3">{{$s.Expected}}</textarea>
</div> </div>
</div> </div>
<div class="form-group row"> <div class="form-group row{{if eq $s.Type "tcp"}} d-none{{end}}">
<label for="service_response_code" class="col-sm-4 col-form-label">Expected Status Code</label> <label for="service_response_code" class="col-sm-4 col-form-label">Expected Status Code</label>
<div class="col-sm-8"> <div class="col-sm-8">
<input type="number" name="expected_status" class="form-control" value="{{$s.ExpectedStatus}}" id="service_response_code"> <input type="number" name="expected_status" class="form-control" value="{{$s.ExpectedStatus}}" id="service_response_code">
</div> </div>
</div> </div>
<div class="form-group row"> <div class="form-group row{{if eq $s.Type "http"}} d-none{{end}}">
<label for="service_port" class="col-sm-4 col-form-label">TCP Port</label> <label for="service_port" class="col-sm-4 col-form-label">TCP Port</label>
<div class="col-sm-8"> <div class="col-sm-8">
<input type="number" name="port" class="form-control" value="{{$s.Port}}" id="service_port" placeholder="8080"> <input type="number" name="port" class="form-control" value="{{$s.Port}}" id="service_port" placeholder="8080">
@ -143,6 +143,12 @@
<input type="number" name="interval" class="form-control" value="{{$s.Interval}}" id="service_interval" required> <input type="number" name="interval" class="form-control" value="{{$s.Interval}}" id="service_interval" required>
</div> </div>
</div> </div>
<div class="form-group row">
<label for="service_timeout" class="col-sm-4 col-form-label">Timeout in Seconds</label>
<div class="col-sm-8">
<input type="number" name="timeout" class="form-control" value="{{$s.Timeout}}" id="service_timeout" placeholder="30">
</div>
</div>
<div class="form-group row"> <div class="form-group row">
<div class="col-6"> <div class="col-6">
<button type="submit" class="btn btn-success btn-block">Update Service</button> <button type="submit" class="btn btn-success btn-block">Update Service</button>
@ -155,7 +161,7 @@
</div> </div>
<div class="col-12 mt-4"> <div class="col-12 mt-4{{if eq $s.Type "tcp"}} d-none{{end}}">
<h3>Last Response</h3> <h3>Last Response</h3>
<textarea rows="8" class="form-control" readonly>{{ $s.LastResponse }}</textarea> <textarea rows="8" class="form-control" readonly>{{ $s.LastResponse }}</textarea>
<div class="form-group row mt-2"> <div class="form-group row mt-2">
@ -168,7 +174,7 @@
</div> </div>
<div class="col-12 mt-4"> <div class="col-12 mt-4{{if eq $s.Type "tcp"}} d-none{{end}}">
<h3>Service Checkins</h3> <h3>Service Checkins</h3>
{{ range $s.Checkins }} {{ range $s.Checkins }}
<h5>Check #{{.Id}} <span class="badge online_badge float-right">Checked in {{.Ago}}</span></h5> <h5>Check #{{.Id}} <span class="badge online_badge float-right">Checked in {{.Ago}}</span></h5>

View File

@ -97,10 +97,10 @@
<div class="form-group row"> <div class="form-group row">
<label for="service_response_code" class="col-sm-4 col-form-label">Expected Status Code</label> <label for="service_response_code" class="col-sm-4 col-form-label">Expected Status Code</label>
<div class="col-sm-8"> <div class="col-sm-8">
<input type="number" name="expected_status" class="form-control" id="service_response_code" value="200"> <input type="number" name="expected_status" class="form-control" id="service_response_code" placeholder="200">
</div> </div>
</div> </div>
<div class="form-group row"> <div class="form-group row d-none">
<label for="service_port" class="col-sm-4 col-form-label">TCP Port</label> <label for="service_port" class="col-sm-4 col-form-label">TCP Port</label>
<div class="col-sm-8"> <div class="col-sm-8">
<input type="number" name="port" class="form-control" id="service_port" placeholder="8080"> <input type="number" name="port" class="form-control" id="service_port" placeholder="8080">
@ -112,6 +112,12 @@
<input type="number" name="interval" class="form-control" id="service_interval" value="60" required> <input type="number" name="interval" class="form-control" id="service_interval" value="60" required>
</div> </div>
</div> </div>
<div class="form-group row">
<label for="service_timeout" class="col-sm-4 col-form-label">Timeout in Seconds</label>
<div class="col-sm-8">
<input type="number" name="timeout" class="form-control" id="service_timeout" placeholder="30">
</div>
</div>
<div class="form-group row"> <div class="form-group row">
<div class="col-sm-12"> <div class="col-sm-12">
<button type="submit" class="btn btn-success btn-block">Create Service</button> <button type="submit" class="btn btn-success btn-block">Create Service</button>

View File

@ -79,6 +79,8 @@ type Service struct {
PostData string `db:"post_data" json:"post_data"` PostData string `db:"post_data" json:"post_data"`
Port int `db:"port" json:"port"` Port int `db:"port" json:"port"`
CreatedAt time.Time `db:"created_at" json:"created_at"` CreatedAt time.Time `db:"created_at" json:"created_at"`
Timeout int `db:"timeout" json:"timeout"`
Order int `db:"order_id" json:"order_id"`
Online bool `json:"online"` Online bool `json:"online"`
Latency float64 `json:"latency"` Latency float64 `json:"latency"`
Online24Hours float32 `json:"24_hours_online"` Online24Hours float32 `json:"24_hours_online"`