2020-04-02 20:15:39 -07:00
// This file is part of MinIO Console Server
2021-01-19 17:04:13 -06:00
// Copyright (c) 2021 MinIO, Inc.
2020-04-02 20:15:39 -07:00
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
package restapi
import (
"context"
2021-01-13 14:08:32 -06:00
"encoding/json"
2021-08-17 14:02:42 -05:00
"errors"
2021-01-13 14:08:32 -06:00
"fmt"
2021-08-17 14:02:42 -05:00
"net/http"
2021-01-13 14:08:32 -06:00
"net/url"
"regexp"
"strings"
2022-11-15 19:09:58 +01:00
"sync"
2020-04-03 14:27:47 -07:00
"time"
2020-04-02 20:15:39 -07:00
"github.com/go-openapi/runtime/middleware"
2022-05-31 10:03:58 -05:00
"github.com/go-openapi/swag"
2020-07-26 00:34:17 -07:00
"github.com/minio/console/models"
"github.com/minio/console/restapi/operations"
2022-04-27 11:45:04 -07:00
systemApi "github.com/minio/console/restapi/operations/system"
2020-04-02 20:15:39 -07:00
)
2020-07-26 00:34:17 -07:00
func registerAdminInfoHandlers ( api * operations . ConsoleAPI ) {
2020-04-03 14:27:47 -07:00
// return usage stats
2022-04-27 11:45:04 -07:00
api . SystemAdminInfoHandler = systemApi . AdminInfoHandlerFunc ( func ( params systemApi . AdminInfoParams , session * models . Principal ) middleware . Responder {
2021-11-11 18:04:18 -06:00
infoResp , err := getAdminInfoResponse ( session , params )
2020-04-02 20:15:39 -07:00
if err != nil {
2022-04-27 11:45:04 -07:00
return systemApi . NewAdminInfoDefault ( int ( err . Code ) ) . WithPayload ( err )
2020-04-02 20:15:39 -07:00
}
2022-04-27 11:45:04 -07:00
return systemApi . NewAdminInfoOK ( ) . WithPayload ( infoResp )
2020-04-02 20:15:39 -07:00
} )
2021-05-21 10:24:16 -07:00
// return single widget results
2022-04-27 11:45:04 -07:00
api . SystemDashboardWidgetDetailsHandler = systemApi . DashboardWidgetDetailsHandlerFunc ( func ( params systemApi . DashboardWidgetDetailsParams , session * models . Principal ) middleware . Responder {
2021-05-21 10:24:16 -07:00
infoResp , err := getAdminInfoWidgetResponse ( params )
if err != nil {
2022-04-27 11:45:04 -07:00
return systemApi . NewDashboardWidgetDetailsDefault ( int ( err . Code ) ) . WithPayload ( err )
2021-05-21 10:24:16 -07:00
}
2022-04-27 11:45:04 -07:00
return systemApi . NewDashboardWidgetDetailsOK ( ) . WithPayload ( infoResp )
2021-05-21 10:24:16 -07:00
} )
2020-04-02 20:15:39 -07:00
}
2021-07-19 11:48:50 -07:00
type UsageInfo struct {
2021-12-07 15:39:50 -06:00
Buckets int64
Objects int64
Usage int64
DisksUsage int64
Servers [ ] * models . ServerProperties
EndpointNotReady bool
2022-11-10 22:54:39 +05:30
Backend * models . BackendProperties
2020-04-02 20:15:39 -07:00
}
2021-07-19 11:48:50 -07:00
// GetAdminInfo invokes admin info and returns a parsed `UsageInfo` structure
func GetAdminInfo ( ctx context . Context , client MinioAdmin ) ( * UsageInfo , error ) {
2020-04-02 20:15:39 -07:00
serverInfo , err := client . serverInfo ( ctx )
if err != nil {
return nil , err
}
// we are trimming uint64 to int64 this will report an incorrect measurement for numbers greater than
// 9,223,372,036,854,775,807
2020-07-09 12:24:01 -07:00
2022-11-10 22:54:39 +05:30
var backendType string
var rrSCParity float64
var standardSCParity float64
if v , success := serverInfo . Backend . ( map [ string ] interface { } ) ; success {
bt , ok := v [ "backendType" ]
if ok {
backendType = bt . ( string )
}
rp , ok := v [ "rrSCParity" ]
if ok {
rrSCParity = rp . ( float64 )
}
sp , ok := v [ "standardSCParity" ]
if ok {
standardSCParity = sp . ( float64 )
}
}
2020-07-09 12:24:01 -07:00
var usedSpace int64
2022-05-05 13:44:10 -07:00
// serverArray contains the serverProperties which describe the servers in the network
2021-08-24 11:13:26 -07:00
var serverArray [ ] * models . ServerProperties
for _ , serv := range serverInfo . Servers {
2022-05-05 13:44:10 -07:00
drives := [ ] * models . ServerDrives { }
2021-08-27 12:59:41 -05:00
for _ , drive := range serv . Disks {
2023-01-05 16:54:00 -06:00
usedSpace += int64 ( drive . UsedSpace )
2021-09-01 17:07:15 -07:00
drives = append ( drives , & models . ServerDrives {
State : drive . State ,
UUID : drive . UUID ,
Endpoint : drive . Endpoint ,
RootDisk : drive . RootDisk ,
DrivePath : drive . DrivePath ,
Healing : drive . Healing ,
Model : drive . Model ,
TotalSpace : int64 ( drive . TotalSpace ) ,
UsedSpace : int64 ( drive . UsedSpace ) ,
AvailableSpace : int64 ( drive . AvailableSpace ) ,
} )
2021-08-27 12:59:41 -05:00
}
2022-05-05 13:44:10 -07:00
newServer := & models . ServerProperties {
2021-08-24 11:13:26 -07:00
State : serv . State ,
Endpoint : serv . Endpoint ,
2021-08-27 12:59:41 -05:00
Uptime : serv . Uptime ,
2021-08-24 11:13:26 -07:00
Version : serv . Version ,
CommitID : serv . CommitID ,
PoolNumber : int64 ( serv . PoolNumber ) ,
2021-08-27 12:59:41 -05:00
Network : serv . Network ,
Drives : drives ,
2021-08-24 11:13:26 -07:00
}
serverArray = append ( serverArray , newServer )
}
2022-11-10 22:54:39 +05:30
backendData := & models . BackendProperties {
BackendType : backendType ,
RrSCParity : int64 ( rrSCParity ) ,
StandardSCParity : int64 ( standardSCParity ) ,
}
2021-07-19 11:48:50 -07:00
return & UsageInfo {
2020-07-09 12:24:01 -07:00
Buckets : int64 ( serverInfo . Buckets . Count ) ,
Objects : int64 ( serverInfo . Objects . Count ) ,
Usage : int64 ( serverInfo . Usage . Size ) ,
DisksUsage : usedSpace ,
2021-08-24 11:13:26 -07:00
Servers : serverArray ,
2022-11-10 22:54:39 +05:30
Backend : backendData ,
2020-04-02 20:15:39 -07:00
} , nil
}
2021-01-13 14:08:32 -06:00
type Target struct {
Expr string
Interval string
LegendFormat string
2021-05-10 20:12:15 -07:00
Step int32
2022-06-09 17:52:12 -05:00
InitialTime int64
2021-01-13 14:08:32 -06:00
}
type ReduceOptions struct {
Calcs [ ] string
}
type MetricOptions struct {
ReduceOptions ReduceOptions
}
type Metric struct {
2021-05-12 15:35:14 -05:00
ID int32
Title string
Type string
Options MetricOptions
Targets [ ] Target
GridPos GridPos
MaxDataPoints int32
}
type GridPos struct {
H int32
W int32
X int32
Y int32
2021-01-13 14:08:32 -06:00
}
type WidgetLabel struct {
Name string
}
var labels = [ ] WidgetLabel {
{ Name : "instance" } ,
{ Name : "disk" } ,
2021-05-18 09:06:31 -07:00
{ Name : "server" } ,
{ Name : "api" } ,
2021-01-13 14:08:32 -06:00
}
var widgets = [ ] Metric {
{
2021-05-12 15:35:14 -05:00
ID : 1 ,
Title : "Uptime" ,
Type : "stat" ,
MaxDataPoints : 100 ,
GridPos : GridPos {
H : 6 ,
W : 3 ,
X : 0 ,
Y : 0 ,
} ,
2021-01-13 14:08:32 -06:00
Options : MetricOptions {
ReduceOptions : ReduceOptions {
Calcs : [ ] string {
"mean" ,
} ,
} ,
} ,
Targets : [ ] Target {
{
2022-05-05 13:44:10 -07:00
Expr : ` time() - max(minio_node_process_starttime_seconds { $__query}) ` ,
2021-01-13 14:08:32 -06:00
LegendFormat : "{{instance}}" ,
2021-05-10 20:12:15 -07:00
Step : 60 ,
2021-01-13 14:08:32 -06:00
} ,
} ,
} ,
{
2021-05-12 15:35:14 -05:00
ID : 65 ,
Title : "Total S3 Traffic Inbound" ,
Type : "stat" ,
MaxDataPoints : 100 ,
GridPos : GridPos {
H : 3 ,
W : 3 ,
X : 3 ,
Y : 0 ,
} ,
2021-01-13 14:08:32 -06:00
Options : MetricOptions {
ReduceOptions : ReduceOptions {
Calcs : [ ] string {
2021-05-10 20:12:15 -07:00
"last" ,
2021-01-13 14:08:32 -06:00
} ,
} ,
} ,
Targets : [ ] Target {
{
2022-05-05 13:44:10 -07:00
Expr : ` sum by (instance) (minio_s3_traffic_received_bytes { $__query}) ` ,
2021-05-10 20:12:15 -07:00
LegendFormat : "{{instance}}" ,
Step : 60 ,
2021-01-13 14:08:32 -06:00
} ,
} ,
} ,
{
2021-05-12 15:35:14 -05:00
ID : 50 ,
2022-05-31 10:03:58 -05:00
Title : "Current Usable Free Capacity" ,
2021-05-12 15:35:14 -05:00
Type : "gauge" ,
MaxDataPoints : 100 ,
GridPos : GridPos {
H : 6 ,
W : 3 ,
X : 6 ,
Y : 0 ,
} ,
2021-01-13 14:08:32 -06:00
Options : MetricOptions {
ReduceOptions : ReduceOptions {
Calcs : [ ] string {
"lastNotNull" ,
} ,
} ,
} ,
Targets : [ ] Target {
2022-05-31 10:03:58 -05:00
{
Expr : ` topk(1, sum(minio_cluster_capacity_usable_total_bytes { $__query}) by (instance)) ` ,
LegendFormat : "Total Usable" ,
Step : 300 ,
} ,
2021-01-13 14:08:32 -06:00
{
2022-05-05 13:44:10 -07:00
Expr : ` topk(1, sum(minio_cluster_capacity_usable_free_bytes { $__query}) by (instance)) ` ,
2022-05-31 10:03:58 -05:00
LegendFormat : "Usable Free" ,
Step : 300 ,
} ,
{
Expr : ` topk(1, sum(minio_cluster_capacity_usable_total_bytes { $__query}) by (instance)) - topk(1, sum(minio_cluster_capacity_usable_free_bytes { $__query}) by (instance)) ` ,
LegendFormat : "Used Space" ,
Step : 300 ,
} ,
} ,
} ,
{
ID : 51 ,
Title : "Current Usable Total Bytes" ,
Type : "gauge" ,
MaxDataPoints : 100 ,
GridPos : GridPos {
H : 6 ,
W : 3 ,
X : 6 ,
Y : 0 ,
} ,
Options : MetricOptions {
ReduceOptions : ReduceOptions {
Calcs : [ ] string {
"lastNotNull" ,
} ,
} ,
} ,
Targets : [ ] Target {
{
Expr : ` topk(1, sum(minio_cluster_capacity_usable_total_bytes { $__query}) by (instance)) ` ,
2021-01-13 14:08:32 -06:00
LegendFormat : "" ,
2021-05-10 20:12:15 -07:00
Step : 300 ,
2021-01-13 14:08:32 -06:00
} ,
} ,
} ,
{
2021-05-10 20:12:15 -07:00
ID : 68 ,
Title : "Data Usage Growth" ,
2021-01-13 14:08:32 -06:00
Type : "graph" ,
2021-05-12 15:35:14 -05:00
GridPos : GridPos {
H : 6 ,
W : 7 ,
X : 9 ,
Y : 0 ,
} ,
2021-01-13 14:08:32 -06:00
Targets : [ ] Target {
{
2022-05-05 13:44:10 -07:00
Expr : ` sum(minio_bucket_usage_total_bytes { $__query}) by (instance) ` ,
2021-05-10 20:12:15 -07:00
LegendFormat : "Used Capacity" ,
2022-06-09 17:52:12 -05:00
InitialTime : - 180 ,
Step : 10 ,
2021-01-13 14:08:32 -06:00
} ,
} ,
} ,
{
2021-05-10 20:12:15 -07:00
ID : 52 ,
2021-01-13 14:08:32 -06:00
Title : "Object size distribution" ,
Type : "bargauge" ,
2021-05-12 15:35:14 -05:00
GridPos : GridPos {
H : 6 ,
W : 5 ,
X : 16 ,
Y : 0 ,
} ,
2021-01-13 14:08:32 -06:00
Options : MetricOptions {
ReduceOptions : ReduceOptions {
Calcs : [ ] string {
"mean" ,
} ,
} ,
} ,
Targets : [ ] Target {
2021-05-10 20:12:15 -07:00
{
2022-05-05 13:44:10 -07:00
Expr : ` max by (range) (minio_bucket_objects_size_distribution { $__query}) ` ,
2021-05-10 20:12:15 -07:00
LegendFormat : "{{range}}" ,
Step : 300 ,
} ,
} ,
} ,
{
2021-05-12 15:35:14 -05:00
ID : 61 ,
Title : "Total Open FDs" ,
Type : "stat" ,
MaxDataPoints : 100 ,
GridPos : GridPos {
H : 3 ,
W : 3 ,
X : 21 ,
Y : 0 ,
} ,
2021-05-10 20:12:15 -07:00
Options : MetricOptions {
ReduceOptions : ReduceOptions {
Calcs : [ ] string {
"last" ,
} ,
} ,
} ,
Targets : [ ] Target {
{
2022-05-05 13:44:10 -07:00
Expr : ` sum(minio_node_file_descriptor_open_total { $__query}) ` ,
2021-05-10 20:12:15 -07:00
LegendFormat : "" ,
Step : 60 ,
} ,
} ,
} ,
{
2021-05-12 15:35:14 -05:00
ID : 64 ,
Title : "Total S3 Traffic Outbound" ,
Type : "stat" ,
MaxDataPoints : 100 ,
GridPos : GridPos {
H : 3 ,
W : 3 ,
X : 3 ,
Y : 3 ,
} ,
2021-05-10 20:12:15 -07:00
Options : MetricOptions {
ReduceOptions : ReduceOptions {
Calcs : [ ] string {
"last" ,
} ,
} ,
} ,
Targets : [ ] Target {
2021-01-13 14:08:32 -06:00
{
2022-05-05 13:44:10 -07:00
Expr : ` sum by (instance) (minio_s3_traffic_sent_bytes { $__query}) ` ,
2021-05-10 20:12:15 -07:00
LegendFormat : "" ,
Step : 60 ,
2021-01-13 14:08:32 -06:00
} ,
} ,
} ,
{
2021-05-12 15:35:14 -05:00
ID : 62 ,
Title : "Total Goroutines" ,
Type : "stat" ,
MaxDataPoints : 100 ,
GridPos : GridPos {
H : 3 ,
W : 3 ,
X : 21 ,
Y : 3 ,
} ,
2021-05-10 20:12:15 -07:00
Options : MetricOptions {
ReduceOptions : ReduceOptions {
Calcs : [ ] string {
"last" ,
} ,
} ,
} ,
2021-01-13 14:08:32 -06:00
Targets : [ ] Target {
{
2022-05-05 13:44:10 -07:00
Expr : ` sum without (server,instance) (minio_node_go_routine_total { $__query}) ` ,
2021-05-10 20:12:15 -07:00
LegendFormat : "" ,
Step : 60 ,
2021-01-13 14:08:32 -06:00
} ,
} ,
} ,
{
2021-05-12 15:35:14 -05:00
ID : 53 ,
Title : "Total Online Servers" ,
Type : "stat" ,
MaxDataPoints : 100 ,
GridPos : GridPos {
H : 2 ,
W : 3 ,
X : 0 ,
Y : 6 ,
} ,
2021-01-13 14:08:32 -06:00
Options : MetricOptions {
ReduceOptions : ReduceOptions {
Calcs : [ ] string {
"mean" ,
} ,
} ,
} ,
Targets : [ ] Target {
{
2022-05-05 13:44:10 -07:00
Expr : ` minio_cluster_nodes_online_total { $__query} ` ,
2021-01-13 14:08:32 -06:00
LegendFormat : "" ,
2021-05-10 20:12:15 -07:00
Step : 60 ,
2021-01-13 14:08:32 -06:00
} ,
} ,
} ,
{
2021-05-12 15:35:14 -05:00
ID : 9 ,
Title : "Total Online Disks" ,
Type : "stat" ,
MaxDataPoints : 100 ,
GridPos : GridPos {
H : 2 ,
W : 3 ,
X : 3 ,
Y : 6 ,
} ,
2021-01-13 14:08:32 -06:00
Options : MetricOptions {
ReduceOptions : ReduceOptions {
Calcs : [ ] string {
"mean" ,
} ,
} ,
} ,
Targets : [ ] Target {
{
2022-05-05 13:44:10 -07:00
Expr : ` minio_cluster_disk_online_total { $__query} ` ,
2021-05-10 20:12:15 -07:00
LegendFormat : "Total online disks in MinIO Cluster" ,
Step : 60 ,
2021-01-13 14:08:32 -06:00
} ,
} ,
} ,
{
2021-05-12 15:35:14 -05:00
ID : 66 ,
Title : "Number of Buckets" ,
Type : "stat" ,
MaxDataPoints : 100 ,
GridPos : GridPos {
H : 3 ,
W : 3 ,
X : 6 ,
Y : 6 ,
} ,
2021-01-13 14:08:32 -06:00
Options : MetricOptions {
ReduceOptions : ReduceOptions {
Calcs : [ ] string {
"lastNotNull" ,
} ,
} ,
} ,
Targets : [ ] Target {
{
2022-05-05 13:44:10 -07:00
Expr : ` count(count by (bucket) (minio_bucket_usage_total_bytes { $__query})) ` ,
2021-01-13 14:08:32 -06:00
LegendFormat : "" ,
} ,
} ,
} ,
{
2021-05-10 20:12:15 -07:00
ID : 63 ,
Title : "S3 API Data Received Rate " ,
2021-01-13 14:08:32 -06:00
Type : "graph" ,
2021-05-12 15:35:14 -05:00
GridPos : GridPos {
H : 6 ,
W : 7 ,
X : 9 ,
Y : 6 ,
} ,
2021-01-13 14:08:32 -06:00
Targets : [ ] Target {
{
2022-05-05 13:44:10 -07:00
Expr : ` sum by (server) (rate(minio_s3_traffic_received_bytes { $__query}[$__rate_interval])) ` ,
2021-05-10 20:12:15 -07:00
LegendFormat : "Data Received [{{server}}]" ,
2021-01-13 14:08:32 -06:00
} ,
2021-05-10 20:12:15 -07:00
} ,
} ,
{
ID : 70 ,
Title : "S3 API Data Sent Rate " ,
Type : "graph" ,
2021-05-12 15:35:14 -05:00
GridPos : GridPos {
H : 6 ,
W : 8 ,
X : 16 ,
Y : 6 ,
} ,
2021-05-10 20:12:15 -07:00
Targets : [ ] Target {
2021-01-13 14:08:32 -06:00
{
2022-05-05 13:44:10 -07:00
Expr : ` sum by (server) (rate(minio_s3_traffic_sent_bytes { $__query}[$__rate_interval])) ` ,
2021-05-10 20:12:15 -07:00
LegendFormat : "Data Sent [{{server}}]" ,
2021-01-13 14:08:32 -06:00
} ,
} ,
} ,
{
2021-05-12 15:35:14 -05:00
ID : 69 ,
Title : "Total Offline Servers" ,
Type : "stat" ,
MaxDataPoints : 100 ,
GridPos : GridPos {
H : 2 ,
W : 3 ,
X : 0 ,
Y : 8 ,
} ,
2021-01-13 14:08:32 -06:00
Options : MetricOptions {
ReduceOptions : ReduceOptions {
Calcs : [ ] string {
"mean" ,
} ,
} ,
} ,
Targets : [ ] Target {
{
2022-05-05 13:44:10 -07:00
Expr : ` minio_cluster_nodes_offline_total { $__query} ` ,
2021-01-13 14:08:32 -06:00
LegendFormat : "" ,
2021-05-10 20:12:15 -07:00
Step : 60 ,
2021-01-13 14:08:32 -06:00
} ,
} ,
} ,
{
2021-05-12 15:35:14 -05:00
ID : 78 ,
Title : "Total Offline Disks" ,
Type : "stat" ,
MaxDataPoints : 100 ,
GridPos : GridPos {
H : 2 ,
W : 3 ,
X : 3 ,
Y : 8 ,
} ,
2021-01-13 14:08:32 -06:00
Options : MetricOptions {
ReduceOptions : ReduceOptions {
Calcs : [ ] string {
"mean" ,
} ,
} ,
} ,
Targets : [ ] Target {
{
2022-05-05 13:44:10 -07:00
Expr : ` minio_cluster_disk_offline_total { $__query} ` ,
2021-01-13 14:08:32 -06:00
LegendFormat : "" ,
2021-05-10 20:12:15 -07:00
Step : 60 ,
2021-01-13 14:08:32 -06:00
} ,
} ,
} ,
{
2021-05-12 15:35:14 -05:00
ID : 44 ,
Title : "Number of Objects" ,
Type : "stat" ,
MaxDataPoints : 100 ,
GridPos : GridPos {
H : 3 ,
W : 3 ,
X : 6 ,
Y : 9 ,
} ,
2021-01-13 14:08:32 -06:00
Options : MetricOptions {
ReduceOptions : ReduceOptions {
Calcs : [ ] string {
"lastNotNull" ,
} ,
} ,
} ,
Targets : [ ] Target {
{
2022-05-05 13:44:10 -07:00
Expr : ` topk(1, sum(minio_bucket_usage_object_total { $__query}) by (instance)) ` ,
2021-01-13 14:08:32 -06:00
LegendFormat : "" ,
} ,
} ,
} ,
{
2021-05-12 15:35:14 -05:00
ID : 80 ,
Title : "Time Since Last Heal Activity" ,
Type : "stat" ,
MaxDataPoints : 100 ,
GridPos : GridPos {
H : 2 ,
W : 3 ,
X : 0 ,
Y : 10 ,
} ,
2021-01-13 14:08:32 -06:00
Options : MetricOptions {
ReduceOptions : ReduceOptions {
Calcs : [ ] string {
2021-05-10 20:12:15 -07:00
"last" ,
2021-01-13 14:08:32 -06:00
} ,
} ,
} ,
Targets : [ ] Target {
{
2022-05-05 13:44:10 -07:00
Expr : ` minio_heal_time_last_activity_nano_seconds { $__query} ` ,
2021-05-10 20:12:15 -07:00
LegendFormat : "{{server}}" ,
Step : 60 ,
} ,
} ,
} ,
{
2021-05-12 15:35:14 -05:00
ID : 81 ,
Title : "Time Since Last Scan Activity" ,
Type : "stat" ,
MaxDataPoints : 100 ,
GridPos : GridPos {
H : 2 ,
W : 3 ,
X : 3 ,
Y : 10 ,
} ,
2021-05-10 20:12:15 -07:00
Options : MetricOptions {
ReduceOptions : ReduceOptions {
Calcs : [ ] string {
"last" ,
} ,
} ,
} ,
Targets : [ ] Target {
{
2022-05-05 13:44:10 -07:00
Expr : ` minio_usage_last_activity_nano_seconds { $__query} ` ,
2021-05-10 20:12:15 -07:00
LegendFormat : "{{server}}" ,
Step : 60 ,
2021-01-13 14:08:32 -06:00
} ,
} ,
} ,
{
2021-05-10 20:12:15 -07:00
ID : 60 ,
Title : "S3 API Request Rate" ,
2021-01-13 14:08:32 -06:00
Type : "graph" ,
2021-05-12 15:35:14 -05:00
GridPos : GridPos {
H : 10 ,
W : 12 ,
X : 0 ,
Y : 12 ,
} ,
2021-01-13 14:08:32 -06:00
Targets : [ ] Target {
{
2022-05-05 13:44:10 -07:00
Expr : ` sum by (server,api) (increase(minio_s3_requests_total { $__query}[$__rate_interval])) ` ,
2021-05-10 20:12:15 -07:00
LegendFormat : "{{server,api}}" ,
2021-01-13 14:08:32 -06:00
} ,
2021-05-10 20:12:15 -07:00
} ,
} ,
{
ID : 71 ,
Title : "S3 API Request Error Rate" ,
Type : "graph" ,
2021-05-12 15:35:14 -05:00
GridPos : GridPos {
H : 10 ,
W : 12 ,
X : 12 ,
Y : 12 ,
} ,
2021-05-10 20:12:15 -07:00
Targets : [ ] Target {
2021-01-13 14:08:32 -06:00
{
2022-05-05 13:44:10 -07:00
Expr : ` sum by (server,api) (increase(minio_s3_requests_errors_total { $__query}[$__rate_interval])) ` ,
2021-05-10 20:12:15 -07:00
LegendFormat : "{{server,api}}" ,
2021-01-13 14:08:32 -06:00
} ,
} ,
} ,
{
2021-05-10 20:12:15 -07:00
ID : 17 ,
Title : "Internode Data Transfer" ,
2021-01-13 14:08:32 -06:00
Type : "graph" ,
2021-05-12 15:35:14 -05:00
GridPos : GridPos {
H : 8 ,
W : 24 ,
X : 0 ,
Y : 22 ,
} ,
2021-01-13 14:08:32 -06:00
Targets : [ ] Target {
{
2022-05-05 13:44:10 -07:00
Expr : ` rate(minio_inter_node_traffic_sent_bytes { $__query}[$__rate_interval]) ` ,
2021-05-10 20:12:15 -07:00
LegendFormat : "Internode Bytes Received [{{server}}]" ,
Step : 4 ,
2021-01-13 14:08:32 -06:00
} ,
2021-05-10 20:12:15 -07:00
2021-01-13 14:08:32 -06:00
{
2022-05-05 13:44:10 -07:00
Expr : ` rate(minio_inter_node_traffic_sent_bytes { $__query}[$__rate_interval]) ` ,
2021-05-10 20:12:15 -07:00
LegendFormat : "Internode Bytes Received [{{server}}]" ,
2021-01-13 14:08:32 -06:00
} ,
} ,
} ,
{
2021-05-10 20:12:15 -07:00
ID : 77 ,
Title : "Node CPU Usage" ,
2021-01-13 14:08:32 -06:00
Type : "graph" ,
2021-05-12 15:35:14 -05:00
GridPos : GridPos {
H : 9 ,
W : 12 ,
X : 0 ,
Y : 30 ,
} ,
2021-01-13 14:08:32 -06:00
Targets : [ ] Target {
{
2022-05-05 13:44:10 -07:00
Expr : ` rate(minio_node_process_cpu_total_seconds { $__query}[$__rate_interval]) ` ,
2021-05-10 20:12:15 -07:00
LegendFormat : "CPU Usage Rate [{{server}}]" ,
2021-01-13 14:08:32 -06:00
} ,
} ,
} ,
{
2021-05-10 20:12:15 -07:00
ID : 76 ,
Title : "Node Memory Usage" ,
2021-01-13 14:08:32 -06:00
Type : "graph" ,
2021-05-12 15:35:14 -05:00
GridPos : GridPos {
H : 9 ,
W : 12 ,
X : 12 ,
Y : 30 ,
} ,
2021-01-13 14:08:32 -06:00
Targets : [ ] Target {
{
2022-05-05 13:44:10 -07:00
Expr : ` minio_node_process_resident_memory_bytes { $__query} ` ,
2021-05-10 20:12:15 -07:00
LegendFormat : "Memory Used [{{server}}]" ,
2021-01-13 14:08:32 -06:00
} ,
2021-05-10 20:12:15 -07:00
} ,
} ,
{
ID : 74 ,
Title : "Drive Used Capacity" ,
Type : "graph" ,
2021-05-12 15:35:14 -05:00
GridPos : GridPos {
H : 8 ,
W : 12 ,
X : 0 ,
Y : 39 ,
} ,
2021-05-10 20:12:15 -07:00
Targets : [ ] Target {
{
2022-05-05 13:44:10 -07:00
Expr : ` minio_node_disk_used_bytes { $__query} ` ,
2021-05-10 20:12:15 -07:00
LegendFormat : "Used Capacity [{{server}}:{{disk}}]" ,
} ,
} ,
} ,
{
ID : 82 ,
Title : "Drives Free Inodes" ,
Type : "graph" ,
2021-05-12 15:35:14 -05:00
GridPos : GridPos {
H : 8 ,
W : 12 ,
X : 12 ,
Y : 39 ,
} ,
2021-05-10 20:12:15 -07:00
Targets : [ ] Target {
{
2022-05-05 13:44:10 -07:00
Expr : ` minio_cluster_disk_free_inodes { $__query} ` ,
2021-05-10 20:12:15 -07:00
LegendFormat : "Free Inodes [{{server}}:{{disk}}]" ,
} ,
} ,
} ,
{
ID : 11 ,
Title : "Node Syscalls" ,
Type : "graph" ,
2021-05-12 15:35:14 -05:00
GridPos : GridPos {
H : 9 ,
W : 12 ,
X : 0 ,
Y : 47 ,
} ,
2021-05-10 20:12:15 -07:00
Targets : [ ] Target {
2021-01-13 14:08:32 -06:00
{
2022-05-05 13:44:10 -07:00
Expr : ` rate(minio_node_syscall_read_total { $__query}[$__rate_interval]) ` ,
2021-05-10 20:12:15 -07:00
LegendFormat : "Read Syscalls [{{server}}]" ,
Step : 60 ,
} ,
{
2022-05-05 13:44:10 -07:00
Expr : ` rate(minio_node_syscall_read_total { $__query}[$__rate_interval]) ` ,
2021-05-10 20:12:15 -07:00
LegendFormat : "Read Syscalls [{{server}}]" ,
2021-01-13 14:08:32 -06:00
} ,
} ,
} ,
{
2021-05-10 20:12:15 -07:00
ID : 8 ,
Title : "Node File Descriptors" ,
2021-01-13 14:08:32 -06:00
Type : "graph" ,
2021-05-12 15:35:14 -05:00
GridPos : GridPos {
H : 9 ,
W : 12 ,
X : 12 ,
Y : 47 ,
} ,
2021-01-13 14:08:32 -06:00
Targets : [ ] Target {
{
2022-05-05 13:44:10 -07:00
Expr : ` minio_node_file_descriptor_open_total { $__query} ` ,
2021-05-10 20:12:15 -07:00
LegendFormat : "Open FDs [{{server}}]" ,
2021-01-13 14:08:32 -06:00
} ,
} ,
} ,
{
2021-05-10 20:12:15 -07:00
ID : 73 ,
Title : "Node IO" ,
2021-01-13 14:08:32 -06:00
Type : "graph" ,
2021-05-12 15:35:14 -05:00
GridPos : GridPos {
H : 8 ,
W : 24 ,
X : 0 ,
Y : 56 ,
} ,
2021-01-13 14:08:32 -06:00
Targets : [ ] Target {
2021-05-10 20:12:15 -07:00
{
2022-05-05 13:44:10 -07:00
Expr : ` rate(minio_node_io_rchar_bytes { $__query}[$__rate_interval]) ` ,
2021-05-10 20:12:15 -07:00
LegendFormat : "Node RChar [{{server}}]" ,
} ,
2021-01-13 14:08:32 -06:00
{
2022-05-05 13:44:10 -07:00
Expr : ` rate(minio_node_io_rchar_bytes { $__query}[$__rate_interval]) ` ,
2021-05-10 20:12:15 -07:00
LegendFormat : "Node RChar [{{server}}]" ,
2021-01-13 14:08:32 -06:00
} ,
} ,
} ,
}
type Widget struct {
Title string
Type string
}
type DataResult struct {
Metric map [ string ] string ` json:"metric" `
Values [ ] interface { } ` json:"values" `
}
type PromRespData struct {
ResultType string ` json:"resultType" `
Result [ ] DataResult ` json:"result" `
}
2022-05-05 13:44:10 -07:00
2021-01-13 14:08:32 -06:00
type PromResp struct {
Status string ` json:"status" `
Data PromRespData ` json:"data" `
}
type LabelResponse struct {
Status string ` json:"status" `
Data [ ] string ` json:"data" `
}
2022-05-05 13:44:10 -07:00
2021-01-13 14:08:32 -06:00
type LabelResults struct {
Label string
Response LabelResponse
}
2020-04-02 20:15:39 -07:00
// getAdminInfoResponse returns the response containing total buckets, objects and usage.
2022-04-27 11:45:04 -07:00
func getAdminInfoResponse ( session * models . Principal , params systemApi . AdminInfoParams ) ( * models . AdminInfoResponse , * models . Error ) {
2022-04-28 12:55:06 -07:00
ctx , cancel := context . WithCancel ( params . HTTPRequest . Context ( ) )
defer cancel ( )
2021-11-11 18:04:18 -06:00
prometheusURL := ""
if ! * params . DefaultOnly {
prometheusURL = getPrometheusURL ( )
}
2021-07-19 11:48:50 -07:00
mAdmin , err := NewMinioAdminClient ( session )
2021-06-16 14:50:04 -07:00
if err != nil {
2022-04-28 12:55:06 -07:00
return nil , ErrorWithContext ( ctx , err )
2021-06-16 14:50:04 -07:00
}
2023-01-05 16:54:00 -06:00
sessionResp , err2 := getUsageWidgetsForDeployment ( ctx , prometheusURL , AdminClient { Client : mAdmin } )
2021-06-16 14:50:04 -07:00
if err2 != nil {
2022-04-28 12:55:06 -07:00
return nil , ErrorWithContext ( ctx , err2 )
2021-06-16 14:50:04 -07:00
}
return sessionResp , nil
}
2021-01-13 14:08:32 -06:00
2023-01-05 16:54:00 -06:00
func getUsageWidgetsForDeployment ( ctx context . Context , prometheusURL string , adminClient MinioAdmin ) ( * models . AdminInfoResponse , error ) {
2022-10-07 21:19:40 -07:00
prometheusStatus := models . AdminInfoResponseAdvancedMetricsStatusAvailable
if prometheusURL == "" {
prometheusStatus = models . AdminInfoResponseAdvancedMetricsStatusNotConfigured
}
2022-04-28 12:55:06 -07:00
if prometheusURL != "" && ! testPrometheusURL ( ctx , prometheusURL ) {
2022-10-07 21:19:40 -07:00
prometheusStatus = models . AdminInfoResponseAdvancedMetricsStatusUnavailable
}
sessionResp := & models . AdminInfoResponse {
AdvancedMetricsStatus : prometheusStatus ,
2021-12-07 15:39:50 -06:00
}
2022-10-07 21:19:40 -07:00
doneCh := make ( chan error )
go func ( ) {
defer close ( doneCh )
2021-01-13 14:08:32 -06:00
// serialize output
2021-07-19 11:48:50 -07:00
usage , err := GetAdminInfo ( ctx , adminClient )
2021-01-13 14:08:32 -06:00
if err != nil {
2022-10-07 21:19:40 -07:00
doneCh <- err
2021-01-13 14:08:32 -06:00
}
2022-10-13 20:05:59 -07:00
if usage != nil {
sessionResp . Buckets = usage . Buckets
sessionResp . Objects = usage . Objects
sessionResp . Usage = usage . Usage
sessionResp . Servers = usage . Servers
2022-11-10 22:54:39 +05:30
sessionResp . Backend = usage . Backend
2022-10-13 20:05:59 -07:00
}
2022-10-07 21:19:40 -07:00
} ( )
2021-01-13 14:08:32 -06:00
2021-05-21 10:24:16 -07:00
var wdgts [ ] * models . Widget
2022-10-07 21:19:40 -07:00
if prometheusStatus == models . AdminInfoResponseAdvancedMetricsStatusAvailable {
// We will tell the frontend about a list of widgets so it can fetch the ones it wants
for _ , m := range widgets {
wdgtResult := models . Widget {
ID : m . ID ,
Title : m . Title ,
Type : m . Type ,
}
if len ( m . Options . ReduceOptions . Calcs ) > 0 {
wdgtResult . Options = & models . WidgetOptions {
ReduceOptions : & models . WidgetOptionsReduceOptions {
Calcs : m . Options . ReduceOptions . Calcs ,
} ,
}
2021-05-21 10:24:16 -07:00
}
2022-10-07 21:19:40 -07:00
wdgts = append ( wdgts , & wdgtResult )
}
sessionResp . Widgets = wdgts
2021-05-21 10:24:16 -07:00
}
2022-10-07 21:19:40 -07:00
// wait for mc admin info
err := <- doneCh
if err != nil {
return nil , err
}
2021-05-21 10:24:16 -07:00
return sessionResp , nil
}
2022-04-28 12:55:06 -07:00
func unmarshalPrometheus ( ctx context . Context , endpoint string , data interface { } ) bool {
2022-10-17 20:34:05 +01:00
httpClnt := GetConsoleHTTPClient ( endpoint )
2022-11-15 19:09:58 +01:00
req , err := http . NewRequestWithContext ( ctx , http . MethodGet , endpoint , nil )
if err != nil {
ErrorWithContext ( ctx , fmt . Errorf ( "Unable to create the request to fetch labels from prometheus: %w" , err ) )
return true
}
resp , err := httpClnt . Do ( req )
2021-06-04 11:35:55 -07:00
if err != nil {
2022-11-15 19:09:58 +01:00
ErrorWithContext ( ctx , fmt . Errorf ( "Unable to fetch labels from prometheus: %w" , err ) )
2021-06-04 11:35:55 -07:00
return true
}
2022-11-15 19:09:58 +01:00
2021-08-16 12:09:03 -07:00
defer resp . Body . Close ( )
2021-06-04 11:35:55 -07:00
2022-11-15 19:09:58 +01:00
if resp . StatusCode != http . StatusOK {
ErrorWithContext ( ctx , fmt . Errorf ( "Unexpected status code from prometheus (%s)" , resp . Status ) )
2021-06-04 11:35:55 -07:00
return true
}
2021-08-16 12:09:03 -07:00
if err = json . NewDecoder ( resp . Body ) . Decode ( data ) ; err != nil {
2022-11-15 19:09:58 +01:00
ErrorWithContext ( ctx , fmt . Errorf ( "Unexpected error from prometheus: %w" , err ) )
2021-06-04 11:35:55 -07:00
return true
}
return false
}
2022-04-28 12:55:06 -07:00
func testPrometheusURL ( ctx context . Context , url string ) bool {
2021-08-17 14:02:42 -05:00
req , err := http . NewRequestWithContext ( ctx , http . MethodGet , url + "/-/healthy" , nil )
if err != nil {
2022-04-28 12:55:06 -07:00
ErrorWithContext ( ctx , fmt . Errorf ( "error Building Request: (%v)" , err ) )
2021-08-17 14:02:42 -05:00
return false
}
2022-10-17 20:34:05 +01:00
response , err := GetConsoleHTTPClient ( url ) . Do ( req )
2021-08-17 14:02:42 -05:00
if err != nil {
2022-04-28 12:55:06 -07:00
ErrorWithContext ( ctx , fmt . Errorf ( "default Prometheus URL not reachable, trying root testing: (%v)" , err ) )
2022-01-21 14:31:29 -07:00
newTestURL := req . URL . Scheme + "://" + req . URL . Host + "/-/healthy"
req2 , err := http . NewRequestWithContext ( ctx , http . MethodGet , newTestURL , nil )
if err != nil {
2022-04-28 12:55:06 -07:00
ErrorWithContext ( ctx , fmt . Errorf ( "error Building Root Request: (%v)" , err ) )
2022-01-21 14:31:29 -07:00
return false
}
2022-10-17 20:34:05 +01:00
rootResponse , err := GetConsoleHTTPClient ( newTestURL ) . Do ( req2 )
2022-01-21 14:31:29 -07:00
if err != nil {
// URL & Root tests didn't work. Prometheus not reachable
2022-04-28 12:55:06 -07:00
ErrorWithContext ( ctx , fmt . Errorf ( "root Prometheus URL not reachable: (%v)" , err ) )
2022-01-21 14:31:29 -07:00
return false
}
return rootResponse . StatusCode == http . StatusOK
2021-08-17 14:02:42 -05:00
}
return response . StatusCode == http . StatusOK
}
2022-04-27 11:45:04 -07:00
func getAdminInfoWidgetResponse ( params systemApi . DashboardWidgetDetailsParams ) ( * models . WidgetDetails , * models . Error ) {
2022-04-28 12:55:06 -07:00
ctx , cancel := context . WithCancel ( params . HTTPRequest . Context ( ) )
defer cancel ( )
2021-05-21 10:24:16 -07:00
prometheusURL := getPrometheusURL ( )
2021-06-03 18:04:08 -07:00
prometheusJobID := getPrometheusJobID ( )
2022-05-05 13:44:10 -07:00
prometheusExtraLabels := getPrometheusExtraLabels ( )
2021-05-21 10:24:16 -07:00
2022-05-11 11:25:29 -07:00
selector := fmt . Sprintf ( ` job="%s" ` , prometheusJobID )
2022-05-05 13:44:10 -07:00
if strings . TrimSpace ( prometheusExtraLabels ) != "" {
2022-05-11 11:25:29 -07:00
selector = fmt . Sprintf ( ` job="%s",%s ` , prometheusJobID , prometheusExtraLabels )
2022-05-05 13:44:10 -07:00
}
return getWidgetDetails ( ctx , prometheusURL , selector , params . WidgetID , params . Step , params . Start , params . End )
2021-06-16 14:50:04 -07:00
}
2022-05-05 13:44:10 -07:00
func getWidgetDetails ( ctx context . Context , prometheusURL string , selector string , widgetID int32 , step * int32 , start * int64 , end * int64 ) ( * models . WidgetDetails , * models . Error ) {
2023-01-05 16:54:00 -06:00
// We test if prometheus URL is reachable. this is meant to avoid unuseful calls and application hang.
if ! testPrometheusURL ( ctx , prometheusURL ) {
return nil , ErrorWithContext ( ctx , errors . New ( "prometheus URL is unreachable" ) )
}
2021-01-13 14:08:32 -06:00
labelResultsCh := make ( chan LabelResults )
for _ , lbl := range labels {
go func ( lbl WidgetLabel ) {
endpoint := fmt . Sprintf ( "%s/api/v1/label/%s/values" , prometheusURL , lbl . Name )
var response LabelResponse
2022-04-28 12:55:06 -07:00
if unmarshalPrometheus ( ctx , endpoint , & response ) {
2021-01-13 14:08:32 -06:00
return
}
labelResultsCh <- LabelResults { Label : lbl . Name , Response : response }
} ( lbl )
2020-04-02 20:15:39 -07:00
}
2021-01-13 14:08:32 -06:00
labelMap := make ( map [ string ] [ ] string )
// wait for as many goroutines that come back in less than 1 second
LabelsWaitLoop :
for {
select {
case <- time . After ( 1 * time . Second ) :
break LabelsWaitLoop
case res := <- labelResultsCh :
labelMap [ res . Label ] = res . Response . Data
if len ( labelMap ) >= len ( labels ) {
break LabelsWaitLoop
}
}
2020-04-02 20:15:39 -07:00
}
2021-01-13 14:08:32 -06:00
// launch a goroutines per widget
for _ , m := range widgets {
2021-06-16 14:50:04 -07:00
if m . ID != widgetID {
2021-05-21 10:24:16 -07:00
continue
}
2021-01-13 14:08:32 -06:00
2022-11-15 19:09:58 +01:00
var (
wg sync . WaitGroup
targetResults = make ( [ ] * models . ResultTarget , len ( m . Targets ) )
)
2021-05-21 10:24:16 -07:00
// for each target we will launch another goroutine to fetch the values
2022-11-15 19:09:58 +01:00
for idx , target := range m . Targets {
wg . Add ( 1 )
go func ( idx int , target Target , inStep * int32 , inStart * int64 , inEnd * int64 ) {
defer wg . Done ( )
2021-05-21 10:24:16 -07:00
apiType := "query_range"
now := time . Now ( )
2022-06-09 17:52:12 -05:00
var initTime int64 = - 15
if target . InitialTime != 0 {
initTime = target . InitialTime
}
timeCalculated := time . Duration ( initTime * int64 ( time . Minute ) )
extraParamters := fmt . Sprintf ( "&start=%d&end=%d" , now . Add ( timeCalculated ) . Unix ( ) , now . Unix ( ) )
2021-05-21 10:24:16 -07:00
var step int32 = 60
if target . Step > 0 {
step = target . Step
}
2021-06-16 14:50:04 -07:00
if inStep != nil && * inStep > 0 {
step = * inStep
2021-05-21 10:24:16 -07:00
}
if step > 0 {
extraParamters = fmt . Sprintf ( "%s&step=%d" , extraParamters , step )
}
2021-06-16 14:50:04 -07:00
if inStart != nil && inEnd != nil {
extraParamters = fmt . Sprintf ( "&start=%d&end=%d&step=%d" , * inStart , * inEnd , * inStep )
2021-05-21 10:24:16 -07:00
}
2021-01-13 14:08:32 -06:00
2021-09-12 23:07:35 -07:00
// replace the `$__rate_interval` global for step with unit (s for seconds)
queryExpr := strings . ReplaceAll ( target . Expr , "$__rate_interval" , fmt . Sprintf ( "%ds" , 240 ) )
2021-05-21 10:24:16 -07:00
if strings . Contains ( queryExpr , "$" ) {
2022-05-05 13:44:10 -07:00
re := regexp . MustCompile ( ` \$([a-z]+) ` )
2021-01-13 14:08:32 -06:00
2021-05-21 10:24:16 -07:00
for _ , match := range re . FindAllStringSubmatch ( queryExpr , - 1 ) {
if val , ok := labelMap [ match [ 1 ] ] ; ok {
queryExpr = strings . ReplaceAll ( queryExpr , "$" + match [ 1 ] , fmt . Sprintf ( "(%s)" , strings . Join ( val , "|" ) ) )
2021-01-13 14:08:32 -06:00
}
}
2021-05-21 10:24:16 -07:00
}
2021-01-13 14:08:32 -06:00
2022-05-05 13:44:10 -07:00
queryExpr = strings . ReplaceAll ( queryExpr , "$__query" , selector )
2021-06-03 18:04:08 -07:00
endpoint := fmt . Sprintf ( "%s/api/v1/%s?query=%s%s" , prometheusURL , apiType , url . QueryEscape ( queryExpr ) , extraParamters )
2021-01-13 14:08:32 -06:00
2021-05-21 10:24:16 -07:00
var response PromResp
2022-04-28 12:55:06 -07:00
if unmarshalPrometheus ( ctx , endpoint , & response ) {
2021-05-21 10:24:16 -07:00
return
}
2021-01-13 14:08:32 -06:00
2021-05-21 10:24:16 -07:00
targetResult := models . ResultTarget {
LegendFormat : target . LegendFormat ,
ResultType : response . Data . ResultType ,
2021-01-13 14:08:32 -06:00
}
2021-06-04 11:35:55 -07:00
2021-05-21 10:24:16 -07:00
for _ , r := range response . Data . Result {
targetResult . Result = append ( targetResult . Result , & models . WidgetResult {
Metric : r . Metric ,
Values : r . Values ,
} )
2021-01-13 14:08:32 -06:00
}
2022-11-15 19:09:58 +01:00
targetResults [ idx ] = & targetResult
} ( idx , target , step , start , end )
2021-05-21 10:24:16 -07:00
}
2022-11-15 19:09:58 +01:00
wg . Wait ( )
2021-05-21 10:24:16 -07:00
wdgtResult := models . WidgetDetails {
ID : m . ID ,
Title : m . Title ,
Type : m . Type ,
}
if len ( m . Options . ReduceOptions . Calcs ) > 0 {
wdgtResult . Options = & models . WidgetDetailsOptions {
ReduceOptions : & models . WidgetDetailsOptionsReduceOptions {
Calcs : m . Options . ReduceOptions . Calcs ,
} ,
2021-01-13 14:08:32 -06:00
}
}
2022-11-15 19:09:58 +01:00
for _ , res := range targetResults {
if res != nil {
wdgtResult . Targets = append ( wdgtResult . Targets , res )
2021-05-21 10:24:16 -07:00
}
}
return & wdgtResult , nil
}
2021-01-13 14:08:32 -06:00
2021-05-21 10:24:16 -07:00
return nil , & models . Error { Code : 404 , Message : swag . String ( "Widget not found" ) }
2020-04-02 20:15:39 -07:00
}