github.com/minio/console@v1.4.1/api/admin_info.go (about)

     1  // This file is part of MinIO Console Server
     2  // Copyright (c) 2021 MinIO, Inc.
     3  //
     4  // This program is free software: you can redistribute it and/or modify
     5  // it under the terms of the GNU Affero General Public License as published by
     6  // the Free Software Foundation, either version 3 of the License, or
     7  // (at your option) any later version.
     8  //
     9  // This program is distributed in the hope that it will be useful,
    10  // but WITHOUT ANY WARRANTY; without even the implied warranty of
    11  // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    12  // GNU Affero General Public License for more details.
    13  //
    14  // You should have received a copy of the GNU Affero General Public License
    15  // along with this program.  If not, see <http://www.gnu.org/licenses/>.
    16  
    17  package api
    18  
    19  import (
    20  	"context"
    21  	"encoding/json"
    22  	"errors"
    23  	"fmt"
    24  	"net/http"
    25  	"net/url"
    26  	"regexp"
    27  	"strings"
    28  	"sync"
    29  	"time"
    30  
    31  	"github.com/minio/console/pkg/utils"
    32  
    33  	"github.com/go-openapi/runtime/middleware"
    34  	"github.com/minio/console/api/operations"
    35  	systemApi "github.com/minio/console/api/operations/system"
    36  	"github.com/minio/console/models"
    37  )
    38  
    39  func registerAdminInfoHandlers(api *operations.ConsoleAPI) {
    40  	// return usage stats
    41  	api.SystemAdminInfoHandler = systemApi.AdminInfoHandlerFunc(func(params systemApi.AdminInfoParams, session *models.Principal) middleware.Responder {
    42  		infoResp, err := getAdminInfoResponse(session, params)
    43  		if err != nil {
    44  			return systemApi.NewAdminInfoDefault(err.Code).WithPayload(err.APIError)
    45  		}
    46  		return systemApi.NewAdminInfoOK().WithPayload(infoResp)
    47  	})
    48  	// return single widget results
    49  	api.SystemDashboardWidgetDetailsHandler = systemApi.DashboardWidgetDetailsHandlerFunc(func(params systemApi.DashboardWidgetDetailsParams, _ *models.Principal) middleware.Responder {
    50  		infoResp, err := getAdminInfoWidgetResponse(params)
    51  		if err != nil {
    52  			return systemApi.NewDashboardWidgetDetailsDefault(err.Code).WithPayload(err.APIError)
    53  		}
    54  		return systemApi.NewDashboardWidgetDetailsOK().WithPayload(infoResp)
    55  	})
    56  }
    57  
    58  type UsageInfo struct {
    59  	Buckets          int64
    60  	Objects          int64
    61  	Usage            int64
    62  	DrivesUsage      int64
    63  	Servers          []*models.ServerProperties
    64  	EndpointNotReady bool
    65  	Backend          *models.BackendProperties
    66  }
    67  
    68  // GetAdminInfo invokes admin info and returns a parsed `UsageInfo` structure
    69  func GetAdminInfo(ctx context.Context, client MinioAdmin) (*UsageInfo, error) {
    70  	serverInfo, err := client.serverInfo(ctx)
    71  	if err != nil {
    72  		return nil, err
    73  	}
    74  	// we are trimming uint64 to int64 this will report an incorrect measurement for numbers greater than
    75  	// 9,223,372,036,854,775,807
    76  
    77  	backendType := serverInfo.Backend.Type
    78  	rrSCParity := serverInfo.Backend.RRSCParity
    79  	standardSCParity := serverInfo.Backend.StandardSCParity
    80  	onlineDrives := serverInfo.Backend.OnlineDisks
    81  	offlineDrives := serverInfo.Backend.OfflineDisks
    82  
    83  	var usedSpace int64
    84  	// serverArray contains the serverProperties which describe the servers in the network
    85  	var serverArray []*models.ServerProperties
    86  	for _, serv := range serverInfo.Servers {
    87  		drives := []*models.ServerDrives{}
    88  
    89  		for _, drive := range serv.Disks {
    90  			usedSpace += int64(drive.UsedSpace)
    91  			drives = append(drives, &models.ServerDrives{
    92  				State:          drive.State,
    93  				UUID:           drive.UUID,
    94  				Endpoint:       drive.Endpoint,
    95  				RootDisk:       drive.RootDisk,
    96  				DrivePath:      drive.DrivePath,
    97  				Healing:        drive.Healing,
    98  				Model:          drive.Model,
    99  				TotalSpace:     int64(drive.TotalSpace),
   100  				UsedSpace:      int64(drive.UsedSpace),
   101  				AvailableSpace: int64(drive.AvailableSpace),
   102  			})
   103  		}
   104  
   105  		newServer := &models.ServerProperties{
   106  			State:      serv.State,
   107  			Endpoint:   serv.Endpoint,
   108  			Uptime:     serv.Uptime,
   109  			Version:    serv.Version,
   110  			CommitID:   serv.CommitID,
   111  			PoolNumber: int64(serv.PoolNumber),
   112  			Network:    serv.Network,
   113  			Drives:     drives,
   114  		}
   115  
   116  		serverArray = append(serverArray, newServer)
   117  	}
   118  
   119  	backendData := &models.BackendProperties{
   120  		BackendType:      string(backendType),
   121  		RrSCParity:       int64(rrSCParity),
   122  		StandardSCParity: int64(standardSCParity),
   123  		OnlineDrives:     int64(onlineDrives),
   124  		OfflineDrives:    int64(offlineDrives),
   125  	}
   126  	return &UsageInfo{
   127  		Buckets:     int64(serverInfo.Buckets.Count),
   128  		Objects:     int64(serverInfo.Objects.Count),
   129  		Usage:       int64(serverInfo.Usage.Size),
   130  		DrivesUsage: usedSpace,
   131  		Servers:     serverArray,
   132  		Backend:     backendData,
   133  	}, nil
   134  }
   135  
   136  type Target struct {
   137  	Expr         string
   138  	Interval     string
   139  	LegendFormat string
   140  	Step         int32
   141  	InitialTime  int64
   142  }
   143  
   144  type ReduceOptions struct {
   145  	Calcs []string
   146  }
   147  
   148  type MetricOptions struct {
   149  	ReduceOptions ReduceOptions
   150  }
   151  
   152  type Metric struct {
   153  	ID            int32
   154  	Title         string
   155  	Type          string
   156  	Options       MetricOptions
   157  	Targets       []Target
   158  	GridPos       GridPos
   159  	MaxDataPoints int32
   160  }
   161  
   162  type GridPos struct {
   163  	H int32
   164  	W int32
   165  	X int32
   166  	Y int32
   167  }
   168  
   169  type WidgetLabel struct {
   170  	Name string
   171  }
   172  
   173  var labels = []WidgetLabel{
   174  	{Name: "instance"},
   175  	{Name: "drive"},
   176  	{Name: "server"},
   177  	{Name: "api"},
   178  }
   179  
   180  var widgets = []Metric{
   181  	{
   182  		ID:            1,
   183  		Title:         "Uptime",
   184  		Type:          "stat",
   185  		MaxDataPoints: 100,
   186  		GridPos: GridPos{
   187  			H: 6,
   188  			W: 3,
   189  			X: 0,
   190  			Y: 0,
   191  		},
   192  		Options: MetricOptions{
   193  			ReduceOptions: ReduceOptions{
   194  				Calcs: []string{
   195  					"mean",
   196  				},
   197  			},
   198  		},
   199  		Targets: []Target{
   200  			{
   201  				Expr:         `time() - max(minio_node_process_starttime_seconds{$__query})`,
   202  				LegendFormat: "{{instance}}",
   203  				Step:         60,
   204  			},
   205  		},
   206  	},
   207  	{
   208  		ID:            65,
   209  		Title:         "Total S3 Traffic Inbound",
   210  		Type:          "stat",
   211  		MaxDataPoints: 100,
   212  		GridPos: GridPos{
   213  			H: 3,
   214  			W: 3,
   215  			X: 3,
   216  			Y: 0,
   217  		},
   218  		Options: MetricOptions{
   219  			ReduceOptions: ReduceOptions{
   220  				Calcs: []string{
   221  					"last",
   222  				},
   223  			},
   224  		},
   225  		Targets: []Target{
   226  			{
   227  				Expr:         `sum by (instance) (minio_s3_traffic_received_bytes{$__query})`,
   228  				LegendFormat: "{{instance}}",
   229  				Step:         60,
   230  			},
   231  		},
   232  	},
   233  	{
   234  		ID:            50,
   235  		Title:         "Current Usable Free Capacity",
   236  		Type:          "gauge",
   237  		MaxDataPoints: 100,
   238  		GridPos: GridPos{
   239  			H: 6,
   240  			W: 3,
   241  			X: 6,
   242  			Y: 0,
   243  		},
   244  		Options: MetricOptions{
   245  			ReduceOptions: ReduceOptions{
   246  				Calcs: []string{
   247  					"lastNotNull",
   248  				},
   249  			},
   250  		},
   251  		Targets: []Target{
   252  			{
   253  				Expr:         `topk(1, sum(minio_cluster_capacity_usable_total_bytes{$__query}) by (instance))`,
   254  				LegendFormat: "Total Usable",
   255  				Step:         300,
   256  			},
   257  			{
   258  				Expr:         `topk(1, sum(minio_cluster_capacity_usable_free_bytes{$__query}) by (instance))`,
   259  				LegendFormat: "Usable Free",
   260  				Step:         300,
   261  			},
   262  			{
   263  				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))`,
   264  				LegendFormat: "Used Space",
   265  				Step:         300,
   266  			},
   267  		},
   268  	},
   269  	{
   270  		ID:            51,
   271  		Title:         "Current Usable Total Bytes",
   272  		Type:          "gauge",
   273  		MaxDataPoints: 100,
   274  		GridPos: GridPos{
   275  			H: 6,
   276  			W: 3,
   277  			X: 6,
   278  			Y: 0,
   279  		},
   280  		Options: MetricOptions{
   281  			ReduceOptions: ReduceOptions{
   282  				Calcs: []string{
   283  					"lastNotNull",
   284  				},
   285  			},
   286  		},
   287  		Targets: []Target{
   288  			{
   289  				Expr:         `topk(1, sum(minio_cluster_capacity_usable_total_bytes{$__query}) by (instance))`,
   290  				LegendFormat: "",
   291  				Step:         300,
   292  			},
   293  		},
   294  	},
   295  	{
   296  		ID:    68,
   297  		Title: "Data Usage Growth",
   298  		Type:  "graph",
   299  		GridPos: GridPos{
   300  			H: 6,
   301  			W: 7,
   302  			X: 9,
   303  			Y: 0,
   304  		},
   305  		Targets: []Target{
   306  			{
   307  				Expr:         `minio_cluster_usage_total_bytes{$__query}`,
   308  				LegendFormat: "Used Capacity",
   309  				InitialTime:  -180,
   310  				Step:         10,
   311  			},
   312  		},
   313  	},
   314  	{
   315  		ID:    52,
   316  		Title: "Object size distribution",
   317  		Type:  "bargauge",
   318  		GridPos: GridPos{
   319  			H: 6,
   320  			W: 5,
   321  			X: 16,
   322  			Y: 0,
   323  		},
   324  		Options: MetricOptions{
   325  			ReduceOptions: ReduceOptions{
   326  				Calcs: []string{
   327  					"mean",
   328  				},
   329  			},
   330  		},
   331  		Targets: []Target{
   332  			{
   333  				Expr:         `minio_cluster_objects_size_distribution{$__query}`,
   334  				LegendFormat: "{{range}}",
   335  				Step:         300,
   336  			},
   337  		},
   338  	},
   339  	{
   340  		ID:            61,
   341  		Title:         "Total Open FDs",
   342  		Type:          "stat",
   343  		MaxDataPoints: 100,
   344  		GridPos: GridPos{
   345  			H: 3,
   346  			W: 3,
   347  			X: 21,
   348  			Y: 0,
   349  		},
   350  		Options: MetricOptions{
   351  			ReduceOptions: ReduceOptions{
   352  				Calcs: []string{
   353  					"last",
   354  				},
   355  			},
   356  		},
   357  		Targets: []Target{
   358  			{
   359  				Expr:         `sum(minio_node_file_descriptor_open_total{$__query})`,
   360  				LegendFormat: "",
   361  				Step:         60,
   362  			},
   363  		},
   364  	},
   365  	{
   366  		ID:            64,
   367  		Title:         "Total S3 Traffic Outbound",
   368  		Type:          "stat",
   369  		MaxDataPoints: 100,
   370  		GridPos: GridPos{
   371  			H: 3,
   372  			W: 3,
   373  			X: 3,
   374  			Y: 3,
   375  		},
   376  		Options: MetricOptions{
   377  			ReduceOptions: ReduceOptions{
   378  				Calcs: []string{
   379  					"last",
   380  				},
   381  			},
   382  		},
   383  		Targets: []Target{
   384  			{
   385  				Expr:         `sum by (instance) (minio_s3_traffic_sent_bytes{$__query})`,
   386  				LegendFormat: "",
   387  				Step:         60,
   388  			},
   389  		},
   390  	},
   391  	{
   392  		ID:            62,
   393  		Title:         "Total Goroutines",
   394  		Type:          "stat",
   395  		MaxDataPoints: 100,
   396  		GridPos: GridPos{
   397  			H: 3,
   398  			W: 3,
   399  			X: 21,
   400  			Y: 3,
   401  		},
   402  		Options: MetricOptions{
   403  			ReduceOptions: ReduceOptions{
   404  				Calcs: []string{
   405  					"last",
   406  				},
   407  			},
   408  		},
   409  		Targets: []Target{
   410  			{
   411  				Expr:         `sum without (server,instance) (minio_node_go_routine_total{$__query})`,
   412  				LegendFormat: "",
   413  				Step:         60,
   414  			},
   415  		},
   416  	},
   417  	{
   418  		ID:            53,
   419  		Title:         "Total Online Servers",
   420  		Type:          "stat",
   421  		MaxDataPoints: 100,
   422  		GridPos: GridPos{
   423  			H: 2,
   424  			W: 3,
   425  			X: 0,
   426  			Y: 6,
   427  		},
   428  		Options: MetricOptions{
   429  			ReduceOptions: ReduceOptions{
   430  				Calcs: []string{
   431  					"mean",
   432  				},
   433  			},
   434  		},
   435  		Targets: []Target{
   436  			{
   437  				Expr:         `minio_cluster_nodes_online_total{$__query}`,
   438  				LegendFormat: "",
   439  				Step:         60,
   440  			},
   441  		},
   442  	},
   443  	{
   444  		ID:            9,
   445  		Title:         "Total Online Drives",
   446  		Type:          "stat",
   447  		MaxDataPoints: 100,
   448  		GridPos: GridPos{
   449  			H: 2,
   450  			W: 3,
   451  			X: 3,
   452  			Y: 6,
   453  		},
   454  		Options: MetricOptions{
   455  			ReduceOptions: ReduceOptions{
   456  				Calcs: []string{
   457  					"mean",
   458  				},
   459  			},
   460  		},
   461  		Targets: []Target{
   462  			{
   463  				Expr:         `minio_cluster_drive_online_total{$__query}`,
   464  				LegendFormat: "Total online drives in MinIO Cluster",
   465  				Step:         60,
   466  			},
   467  		},
   468  	},
   469  	{
   470  		ID:            66,
   471  		Title:         "Number of Buckets",
   472  		Type:          "stat",
   473  		MaxDataPoints: 5,
   474  		GridPos: GridPos{
   475  			H: 3,
   476  			W: 3,
   477  			X: 6,
   478  			Y: 6,
   479  		},
   480  		Options: MetricOptions{
   481  			ReduceOptions: ReduceOptions{
   482  				Calcs: []string{
   483  					"lastNotNull",
   484  				},
   485  			},
   486  		},
   487  		Targets: []Target{
   488  			{
   489  				Expr:         `minio_cluster_bucket_total{$__query}`,
   490  				LegendFormat: "",
   491  				Step:         100,
   492  			},
   493  		},
   494  	},
   495  	{
   496  		ID:    63,
   497  		Title: "S3 API Data Received Rate ",
   498  		Type:  "graph",
   499  		GridPos: GridPos{
   500  			H: 6,
   501  			W: 7,
   502  			X: 9,
   503  			Y: 6,
   504  		},
   505  		Targets: []Target{
   506  			{
   507  				Expr:         `sum by (server) (rate(minio_s3_traffic_received_bytes{$__query}[$__rate_interval]))`,
   508  				LegendFormat: "Data Received [{{server}}]",
   509  			},
   510  		},
   511  	},
   512  	{
   513  		ID:    70,
   514  		Title: "S3 API Data Sent Rate ",
   515  		Type:  "graph",
   516  		GridPos: GridPos{
   517  			H: 6,
   518  			W: 8,
   519  			X: 16,
   520  			Y: 6,
   521  		},
   522  		Targets: []Target{
   523  			{
   524  				Expr:         `sum by (server) (rate(minio_s3_traffic_sent_bytes{$__query}[$__rate_interval]))`,
   525  				LegendFormat: "Data Sent [{{server}}]",
   526  			},
   527  		},
   528  	},
   529  	{
   530  		ID:            69,
   531  		Title:         "Total Offline Servers",
   532  		Type:          "stat",
   533  		MaxDataPoints: 100,
   534  		GridPos: GridPos{
   535  			H: 2,
   536  			W: 3,
   537  			X: 0,
   538  			Y: 8,
   539  		},
   540  		Options: MetricOptions{
   541  			ReduceOptions: ReduceOptions{
   542  				Calcs: []string{
   543  					"mean",
   544  				},
   545  			},
   546  		},
   547  		Targets: []Target{
   548  			{
   549  				Expr:         `minio_cluster_nodes_offline_total{$__query}`,
   550  				LegendFormat: "",
   551  				Step:         60,
   552  			},
   553  		},
   554  	},
   555  	{
   556  		ID:            78,
   557  		Title:         "Total Offline Drives",
   558  		Type:          "stat",
   559  		MaxDataPoints: 100,
   560  		GridPos: GridPos{
   561  			H: 2,
   562  			W: 3,
   563  			X: 3,
   564  			Y: 8,
   565  		},
   566  		Options: MetricOptions{
   567  			ReduceOptions: ReduceOptions{
   568  				Calcs: []string{
   569  					"mean",
   570  				},
   571  			},
   572  		},
   573  		Targets: []Target{
   574  			{
   575  				Expr:         `minio_cluster_drive_offline_total{$__query}`,
   576  				LegendFormat: "",
   577  				Step:         60,
   578  			},
   579  		},
   580  	},
   581  	{
   582  		ID:            44,
   583  		Title:         "Number of Objects",
   584  		Type:          "stat",
   585  		MaxDataPoints: 100,
   586  		GridPos: GridPos{
   587  			H: 3,
   588  			W: 3,
   589  			X: 6,
   590  			Y: 9,
   591  		},
   592  		Options: MetricOptions{
   593  			ReduceOptions: ReduceOptions{
   594  				Calcs: []string{
   595  					"lastNotNull",
   596  				},
   597  			},
   598  		},
   599  		Targets: []Target{
   600  			{
   601  				Expr:         `minio_cluster_usage_object_total{$__query}`,
   602  				LegendFormat: "",
   603  			},
   604  		},
   605  	},
   606  	{
   607  		ID:            80,
   608  		Title:         "Time Since Last Heal Activity",
   609  		Type:          "stat",
   610  		MaxDataPoints: 100,
   611  		GridPos: GridPos{
   612  			H: 2,
   613  			W: 3,
   614  			X: 0,
   615  			Y: 10,
   616  		},
   617  		Options: MetricOptions{
   618  			ReduceOptions: ReduceOptions{
   619  				Calcs: []string{
   620  					"last",
   621  				},
   622  			},
   623  		},
   624  		Targets: []Target{
   625  			{
   626  				Expr:         `minio_heal_time_last_activity_nano_seconds{$__query}`,
   627  				LegendFormat: "{{server}}",
   628  				Step:         60,
   629  			},
   630  		},
   631  	},
   632  	{
   633  		ID:            81,
   634  		Title:         "Time Since Last Scan Activity",
   635  		Type:          "stat",
   636  		MaxDataPoints: 100,
   637  		GridPos: GridPos{
   638  			H: 2,
   639  			W: 3,
   640  			X: 3,
   641  			Y: 10,
   642  		},
   643  		Options: MetricOptions{
   644  			ReduceOptions: ReduceOptions{
   645  				Calcs: []string{
   646  					"last",
   647  				},
   648  			},
   649  		},
   650  		Targets: []Target{
   651  			{
   652  				Expr:         `minio_usage_last_activity_nano_seconds{$__query}`,
   653  				LegendFormat: "{{server}}",
   654  				Step:         60,
   655  			},
   656  		},
   657  	},
   658  	{
   659  		ID:    60,
   660  		Title: "S3 API Request Rate",
   661  		Type:  "graph",
   662  		GridPos: GridPos{
   663  			H: 10,
   664  			W: 12,
   665  			X: 0,
   666  			Y: 12,
   667  		},
   668  		Targets: []Target{
   669  			{
   670  				Expr:         `sum by (server,api) (increase(minio_s3_requests_total{$__query}[$__rate_interval]))`,
   671  				LegendFormat: "{{server,api}}",
   672  			},
   673  		},
   674  	},
   675  	{
   676  		ID:    71,
   677  		Title: "S3 API Request Error Rate",
   678  		Type:  "graph",
   679  		GridPos: GridPos{
   680  			H: 10,
   681  			W: 12,
   682  			X: 12,
   683  			Y: 12,
   684  		},
   685  		Targets: []Target{
   686  			{
   687  				Expr:         `sum by (server,api) (increase(minio_s3_requests_errors_total{$__query}[$__rate_interval]))`,
   688  				LegendFormat: "{{server,api}}",
   689  			},
   690  		},
   691  	},
   692  	{
   693  		ID:    17,
   694  		Title: "Internode Data Transfer",
   695  		Type:  "graph",
   696  		GridPos: GridPos{
   697  			H: 8,
   698  			W: 24,
   699  			X: 0,
   700  			Y: 22,
   701  		},
   702  		Targets: []Target{
   703  			{
   704  				Expr:         `rate(minio_inter_node_traffic_sent_bytes{$__query}[$__rate_interval])`,
   705  				LegendFormat: "Internode Bytes Received [{{server}}]",
   706  				Step:         4,
   707  			},
   708  
   709  			{
   710  				Expr:         `rate(minio_inter_node_traffic_sent_bytes{$__query}[$__rate_interval])`,
   711  				LegendFormat: "Internode Bytes Received [{{server}}]",
   712  			},
   713  		},
   714  	},
   715  	{
   716  		ID:    77,
   717  		Title: "Node CPU Usage",
   718  		Type:  "graph",
   719  		GridPos: GridPos{
   720  			H: 9,
   721  			W: 12,
   722  			X: 0,
   723  			Y: 30,
   724  		},
   725  		Targets: []Target{
   726  			{
   727  				Expr:         `rate(minio_node_process_cpu_total_seconds{$__query}[$__rate_interval])`,
   728  				LegendFormat: "CPU Usage Rate [{{server}}]",
   729  			},
   730  		},
   731  	},
   732  	{
   733  		ID:    76,
   734  		Title: "Node Memory Usage",
   735  		Type:  "graph",
   736  		GridPos: GridPos{
   737  			H: 9,
   738  			W: 12,
   739  			X: 12,
   740  			Y: 30,
   741  		},
   742  		Targets: []Target{
   743  			{
   744  				Expr:         `minio_node_process_resident_memory_bytes{$__query}`,
   745  				LegendFormat: "Memory Used [{{server}}]",
   746  			},
   747  		},
   748  	},
   749  	{
   750  		ID:    74,
   751  		Title: "Drive Used Capacity",
   752  		Type:  "graph",
   753  		GridPos: GridPos{
   754  			H: 8,
   755  			W: 12,
   756  			X: 0,
   757  			Y: 39,
   758  		},
   759  		Targets: []Target{
   760  			{
   761  				Expr:         `minio_node_drive_used_bytes{$__query}`,
   762  				LegendFormat: "Used Capacity [{{server}}:{{drive}}]",
   763  			},
   764  		},
   765  	},
   766  	{
   767  		ID:    82,
   768  		Title: "Drives Free Inodes",
   769  		Type:  "graph",
   770  		GridPos: GridPos{
   771  			H: 8,
   772  			W: 12,
   773  			X: 12,
   774  			Y: 39,
   775  		},
   776  		Targets: []Target{
   777  			{
   778  				Expr:         `minio_node_drive_free_inodes{$__query}`,
   779  				LegendFormat: "Free Inodes [{{server}}:{{drive}}]",
   780  			},
   781  		},
   782  	},
   783  	{
   784  		ID:    11,
   785  		Title: "Node Syscalls",
   786  		Type:  "graph",
   787  		GridPos: GridPos{
   788  			H: 9,
   789  			W: 12,
   790  			X: 0,
   791  			Y: 47,
   792  		},
   793  		Targets: []Target{
   794  			{
   795  				Expr:         `rate(minio_node_syscall_read_total{$__query}[$__rate_interval])`,
   796  				LegendFormat: "Read Syscalls [{{server}}]",
   797  				Step:         60,
   798  			},
   799  
   800  			{
   801  				Expr:         `rate(minio_node_syscall_read_total{$__query}[$__rate_interval])`,
   802  				LegendFormat: "Read Syscalls [{{server}}]",
   803  			},
   804  		},
   805  	},
   806  	{
   807  		ID:    8,
   808  		Title: "Node File Descriptors",
   809  		Type:  "graph",
   810  		GridPos: GridPos{
   811  			H: 9,
   812  			W: 12,
   813  			X: 12,
   814  			Y: 47,
   815  		},
   816  		Targets: []Target{
   817  			{
   818  				Expr:         `minio_node_file_descriptor_open_total{$__query}`,
   819  				LegendFormat: "Open FDs [{{server}}]",
   820  			},
   821  		},
   822  	},
   823  	{
   824  		ID:    73,
   825  		Title: "Node IO",
   826  		Type:  "graph",
   827  		GridPos: GridPos{
   828  			H: 8,
   829  			W: 24,
   830  			X: 0,
   831  			Y: 56,
   832  		},
   833  		Targets: []Target{
   834  			{
   835  				Expr:         `rate(minio_node_io_rchar_bytes{$__query}[$__rate_interval])`,
   836  				LegendFormat: "Node RChar [{{server}}]",
   837  			},
   838  
   839  			{
   840  				Expr:         `rate(minio_node_io_wchar_bytes{$__query}[$__rate_interval])`,
   841  				LegendFormat: "Node WChar [{{server}}]",
   842  			},
   843  		},
   844  	},
   845  }
   846  
   847  type Widget struct {
   848  	Title string
   849  	Type  string
   850  }
   851  
   852  type DataResult struct {
   853  	Metric map[string]string `json:"metric"`
   854  	Values []interface{}     `json:"values"`
   855  }
   856  
   857  type PromRespData struct {
   858  	ResultType string       `json:"resultType"`
   859  	Result     []DataResult `json:"result"`
   860  }
   861  
   862  type PromResp struct {
   863  	Status string       `json:"status"`
   864  	Data   PromRespData `json:"data"`
   865  }
   866  
   867  type LabelResponse struct {
   868  	Status string   `json:"status"`
   869  	Data   []string `json:"data"`
   870  }
   871  
   872  type LabelResults struct {
   873  	Label    string
   874  	Response LabelResponse
   875  }
   876  
   877  // getAdminInfoResponse returns the response containing total buckets, objects and usage.
   878  func getAdminInfoResponse(session *models.Principal, params systemApi.AdminInfoParams) (*models.AdminInfoResponse, *CodedAPIError) {
   879  	ctx, cancel := context.WithCancel(params.HTTPRequest.Context())
   880  	defer cancel()
   881  	prometheusURL := ""
   882  
   883  	if !*params.DefaultOnly {
   884  		promURL := getPrometheusURL()
   885  		if promURL != "" {
   886  			prometheusURL = promURL
   887  		}
   888  	}
   889  
   890  	mAdmin, err := NewMinioAdminClient(params.HTTPRequest.Context(), session)
   891  	if err != nil {
   892  		return nil, ErrorWithContext(ctx, err)
   893  	}
   894  
   895  	sessionResp, err2 := getUsageWidgetsForDeployment(ctx, prometheusURL, AdminClient{Client: mAdmin})
   896  	if err2 != nil {
   897  		return nil, ErrorWithContext(ctx, err2)
   898  	}
   899  
   900  	return sessionResp, nil
   901  }
   902  
   903  func getUsageWidgetsForDeployment(ctx context.Context, prometheusURL string, adminClient MinioAdmin) (*models.AdminInfoResponse, error) {
   904  	prometheusStatus := models.AdminInfoResponseAdvancedMetricsStatusAvailable
   905  	if prometheusURL == "" {
   906  		prometheusStatus = models.AdminInfoResponseAdvancedMetricsStatusNotConfigured
   907  	}
   908  	if prometheusURL != "" && !testPrometheusURL(ctx, prometheusURL) {
   909  		prometheusStatus = models.AdminInfoResponseAdvancedMetricsStatusUnavailable
   910  	}
   911  	sessionResp := &models.AdminInfoResponse{
   912  		AdvancedMetricsStatus: prometheusStatus,
   913  	}
   914  	doneCh := make(chan error)
   915  	go func() {
   916  		defer close(doneCh)
   917  		// serialize output
   918  		usage, err := GetAdminInfo(ctx, adminClient)
   919  		if err != nil {
   920  			doneCh <- err
   921  		}
   922  		if usage != nil {
   923  			sessionResp.Buckets = usage.Buckets
   924  			sessionResp.Objects = usage.Objects
   925  			sessionResp.Usage = usage.Usage
   926  			sessionResp.Servers = usage.Servers
   927  			sessionResp.Backend = usage.Backend
   928  		}
   929  	}()
   930  
   931  	var wdgts []*models.Widget
   932  	if prometheusStatus == models.AdminInfoResponseAdvancedMetricsStatusAvailable {
   933  		// We will tell the frontend about a list of widgets so it can fetch the ones it wants
   934  		for _, m := range widgets {
   935  			wdgtResult := models.Widget{
   936  				ID:    m.ID,
   937  				Title: m.Title,
   938  				Type:  m.Type,
   939  			}
   940  			if len(m.Options.ReduceOptions.Calcs) > 0 {
   941  				wdgtResult.Options = &models.WidgetOptions{
   942  					ReduceOptions: &models.WidgetOptionsReduceOptions{
   943  						Calcs: m.Options.ReduceOptions.Calcs,
   944  					},
   945  				}
   946  			}
   947  
   948  			wdgts = append(wdgts, &wdgtResult)
   949  		}
   950  		sessionResp.Widgets = wdgts
   951  	}
   952  
   953  	// wait for mc admin info
   954  	err := <-doneCh
   955  	if err != nil {
   956  		return nil, err
   957  	}
   958  
   959  	return sessionResp, nil
   960  }
   961  
   962  func unmarshalPrometheus(ctx context.Context, httpClnt *http.Client, endpoint string, data interface{}) bool {
   963  	req, err := http.NewRequestWithContext(ctx, http.MethodGet, endpoint, nil)
   964  	if err != nil {
   965  		ErrorWithContext(ctx, fmt.Errorf("Unable to create the request to fetch labels from prometheus: %w", err))
   966  		return true
   967  	}
   968  
   969  	prometheusBearer := getPrometheusAuthToken()
   970  
   971  	if prometheusBearer != "" {
   972  		req.Header.Add("Authorization", fmt.Sprintf("Bearer %s", prometheusBearer))
   973  	}
   974  
   975  	resp, err := httpClnt.Do(req)
   976  	if err != nil {
   977  		ErrorWithContext(ctx, fmt.Errorf("Unable to fetch labels from prometheus: %w", err))
   978  		return true
   979  	}
   980  
   981  	defer resp.Body.Close()
   982  
   983  	if resp.StatusCode != http.StatusOK {
   984  		ErrorWithContext(ctx, fmt.Errorf("Unexpected status code from prometheus (%s)", resp.Status))
   985  		return true
   986  	}
   987  
   988  	if err = json.NewDecoder(resp.Body).Decode(data); err != nil {
   989  		ErrorWithContext(ctx, fmt.Errorf("Unexpected error from prometheus: %w", err))
   990  		return true
   991  	}
   992  
   993  	return false
   994  }
   995  
   996  func testPrometheusURL(ctx context.Context, url string) bool {
   997  	req, err := http.NewRequestWithContext(ctx, http.MethodGet, url+"/-/healthy", nil)
   998  	if err != nil {
   999  		ErrorWithContext(ctx, fmt.Errorf("error Building Request: (%v)", err))
  1000  		return false
  1001  	}
  1002  
  1003  	prometheusBearer := getPrometheusAuthToken()
  1004  	if prometheusBearer != "" {
  1005  		req.Header.Add("Authorization", fmt.Sprintf("Bearer %s", prometheusBearer))
  1006  	}
  1007  
  1008  	clientIP := utils.ClientIPFromContext(ctx)
  1009  	httpClnt := GetConsoleHTTPClient(clientIP)
  1010  
  1011  	response, err := httpClnt.Do(req)
  1012  	if err != nil {
  1013  		ErrorWithContext(ctx, fmt.Errorf("default Prometheus URL not reachable, trying root testing: (%v)", err))
  1014  		newTestURL := req.URL.Scheme + "://" + req.URL.Host + "/-/healthy"
  1015  		req2, err := http.NewRequestWithContext(ctx, http.MethodGet, newTestURL, nil)
  1016  		if err != nil {
  1017  			ErrorWithContext(ctx, fmt.Errorf("error Building Root Request: (%v)", err))
  1018  			return false
  1019  		}
  1020  		rootResponse, err := httpClnt.Do(req2)
  1021  		if err != nil {
  1022  			// URL & Root tests didn't work. Prometheus not reachable
  1023  			ErrorWithContext(ctx, fmt.Errorf("root Prometheus URL not reachable: (%v)", err))
  1024  			return false
  1025  		}
  1026  		return rootResponse.StatusCode == http.StatusOK
  1027  	}
  1028  	return response.StatusCode == http.StatusOK
  1029  }
  1030  
  1031  func getAdminInfoWidgetResponse(params systemApi.DashboardWidgetDetailsParams) (*models.WidgetDetails, *CodedAPIError) {
  1032  	ctx, cancel := context.WithCancel(params.HTTPRequest.Context())
  1033  	defer cancel()
  1034  	prometheusURL := getPrometheusURL()
  1035  	prometheusJobID := getPrometheusJobID()
  1036  	prometheusExtraLabels := getPrometheusExtraLabels()
  1037  
  1038  	selector := fmt.Sprintf(`job="%s"`, prometheusJobID)
  1039  	if strings.TrimSpace(prometheusExtraLabels) != "" {
  1040  		selector = fmt.Sprintf(`job="%s",%s`, prometheusJobID, prometheusExtraLabels)
  1041  	}
  1042  	clientIP := getClientIP(params.HTTPRequest)
  1043  	ctx = context.WithValue(ctx, utils.ContextClientIP, clientIP)
  1044  	return getWidgetDetails(ctx, prometheusURL, selector, params.WidgetID, params.Step, params.Start, params.End)
  1045  }
  1046  
  1047  func getWidgetDetails(ctx context.Context, prometheusURL string, selector string, widgetID int32, step *int32, start *int64, end *int64) (*models.WidgetDetails, *CodedAPIError) {
  1048  	// We test if prometheus URL is reachable. this is meant to avoid unuseful calls and application hang.
  1049  	if !testPrometheusURL(ctx, prometheusURL) {
  1050  		return nil, ErrorWithContext(ctx, errors.New("prometheus URL is unreachable"))
  1051  	}
  1052  	clientIP := utils.ClientIPFromContext(ctx)
  1053  	httpClnt := GetConsoleHTTPClient(clientIP)
  1054  
  1055  	labelResultsCh := make(chan LabelResults)
  1056  
  1057  	for _, lbl := range labels {
  1058  		go func(lbl WidgetLabel) {
  1059  			endpoint := fmt.Sprintf("%s/api/v1/label/%s/values", prometheusURL, lbl.Name)
  1060  
  1061  			var response LabelResponse
  1062  			if unmarshalPrometheus(ctx, httpClnt, endpoint, &response) {
  1063  				return
  1064  			}
  1065  
  1066  			labelResultsCh <- LabelResults{Label: lbl.Name, Response: response}
  1067  		}(lbl)
  1068  	}
  1069  
  1070  	labelMap := make(map[string][]string)
  1071  
  1072  	// wait for as many goroutines that come back in less than 1 second
  1073  LabelsWaitLoop:
  1074  	for {
  1075  		select {
  1076  		case <-time.After(1 * time.Second):
  1077  			break LabelsWaitLoop
  1078  		case res := <-labelResultsCh:
  1079  			labelMap[res.Label] = res.Response.Data
  1080  			if len(labelMap) >= len(labels) {
  1081  				break LabelsWaitLoop
  1082  			}
  1083  		}
  1084  	}
  1085  
  1086  	// launch a goroutines per widget
  1087  
  1088  	for _, m := range widgets {
  1089  		if m.ID != widgetID {
  1090  			continue
  1091  		}
  1092  
  1093  		var (
  1094  			wg            sync.WaitGroup
  1095  			targetResults = make([]*models.ResultTarget, len(m.Targets))
  1096  		)
  1097  
  1098  		// for each target we will launch another goroutine to fetch the values
  1099  		for idx, target := range m.Targets {
  1100  			wg.Add(1)
  1101  			go func(idx int, target Target, inStep *int32, inStart *int64, inEnd *int64) {
  1102  				defer wg.Done()
  1103  
  1104  				apiType := "query_range"
  1105  				now := time.Now()
  1106  
  1107  				var initTime int64 = -15
  1108  
  1109  				if target.InitialTime != 0 {
  1110  					initTime = target.InitialTime
  1111  				}
  1112  
  1113  				timeCalculated := time.Duration(initTime * int64(time.Minute))
  1114  
  1115  				extraParamters := fmt.Sprintf("&start=%d&end=%d", now.Add(timeCalculated).Unix(), now.Unix())
  1116  
  1117  				var step int32 = 60
  1118  				if target.Step > 0 {
  1119  					step = target.Step
  1120  				}
  1121  				if inStep != nil && *inStep > 0 {
  1122  					step = *inStep
  1123  				}
  1124  				if step > 0 {
  1125  					extraParamters = fmt.Sprintf("%s&step=%d", extraParamters, step)
  1126  				}
  1127  
  1128  				if inStart != nil && inEnd != nil {
  1129  					extraParamters = fmt.Sprintf("&start=%d&end=%d&step=%d", *inStart, *inEnd, *inStep)
  1130  				}
  1131  
  1132  				// replace the `$__rate_interval` global for step with unit (s for seconds)
  1133  				queryExpr := strings.ReplaceAll(target.Expr, "$__rate_interval", fmt.Sprintf("%ds", 240))
  1134  				if strings.Contains(queryExpr, "$") {
  1135  					re := regexp.MustCompile(`\$([a-z]+)`)
  1136  
  1137  					for _, match := range re.FindAllStringSubmatch(queryExpr, -1) {
  1138  						if val, ok := labelMap[match[1]]; ok {
  1139  							queryExpr = strings.ReplaceAll(queryExpr, "$"+match[1], fmt.Sprintf("(%s)", strings.Join(val, "|")))
  1140  						}
  1141  					}
  1142  				}
  1143  
  1144  				queryExpr = strings.ReplaceAll(queryExpr, "$__query", selector)
  1145  				endpoint := fmt.Sprintf("%s/api/v1/%s?query=%s%s", prometheusURL, apiType, url.QueryEscape(queryExpr), extraParamters)
  1146  
  1147  				var response PromResp
  1148  				if unmarshalPrometheus(ctx, httpClnt, endpoint, &response) {
  1149  					return
  1150  				}
  1151  
  1152  				targetResult := models.ResultTarget{
  1153  					LegendFormat: target.LegendFormat,
  1154  					ResultType:   response.Data.ResultType,
  1155  				}
  1156  
  1157  				for _, r := range response.Data.Result {
  1158  					targetResult.Result = append(targetResult.Result, &models.WidgetResult{
  1159  						Metric: r.Metric,
  1160  						Values: r.Values,
  1161  					})
  1162  				}
  1163  
  1164  				targetResults[idx] = &targetResult
  1165  			}(idx, target, step, start, end)
  1166  		}
  1167  
  1168  		wg.Wait()
  1169  
  1170  		wdgtResult := models.WidgetDetails{
  1171  			ID:    m.ID,
  1172  			Title: m.Title,
  1173  			Type:  m.Type,
  1174  		}
  1175  		if len(m.Options.ReduceOptions.Calcs) > 0 {
  1176  			wdgtResult.Options = &models.WidgetDetailsOptions{
  1177  				ReduceOptions: &models.WidgetDetailsOptionsReduceOptions{
  1178  					Calcs: m.Options.ReduceOptions.Calcs,
  1179  				},
  1180  			}
  1181  		}
  1182  
  1183  		for _, res := range targetResults {
  1184  			if res != nil {
  1185  				wdgtResult.Targets = append(wdgtResult.Targets, res)
  1186  			}
  1187  		}
  1188  		return &wdgtResult, nil
  1189  	}
  1190  
  1191  	return nil, &CodedAPIError{Code: 404, APIError: &models.APIError{Message: "Widget not found"}}
  1192  }