github.com/kiali/kiali@v1.84.0/business/grafana.go (about)

     1  package business
     2  
     3  import (
     4  	"encoding/json"
     5  	"errors"
     6  	"fmt"
     7  	"net/http"
     8  	"net/url"
     9  	"strings"
    10  	"time"
    11  
    12  	"github.com/kiali/kiali/config"
    13  	"github.com/kiali/kiali/config/dashboards"
    14  	"github.com/kiali/kiali/log"
    15  	"github.com/kiali/kiali/models"
    16  	"github.com/kiali/kiali/status"
    17  	"github.com/kiali/kiali/util/httputil"
    18  )
    19  
    20  type dashboardSupplier func(string, string, *config.Auth) ([]byte, int, error)
    21  
    22  var GrafanaDashboardSupplier = findDashboard
    23  
    24  // GetGrafanaInfo returns the Grafana URL and other info, the HTTP status code (int) and eventually an error
    25  func GetGrafanaInfo(dashboardSupplier dashboardSupplier) (*models.GrafanaInfo, int, error) {
    26  	grafanaConfig := config.Get().ExternalServices.Grafana
    27  	if !grafanaConfig.Enabled {
    28  		return nil, http.StatusNoContent, nil
    29  	}
    30  	conn, code, err := getGrafanaConnectionInfo(&grafanaConfig)
    31  	if err != nil {
    32  		return nil, code, err
    33  	}
    34  
    35  	// Call Grafana REST API to get dashboard urls
    36  	links := []models.ExternalLink{}
    37  	for _, dashboardConfig := range grafanaConfig.Dashboards {
    38  		dashboardPath, err := getDashboardPath(dashboardConfig.Name, conn, dashboardSupplier)
    39  		if err != nil {
    40  			return nil, http.StatusServiceUnavailable, err
    41  		}
    42  		if dashboardPath != "" {
    43  			externalLink := models.ExternalLink{
    44  				URL:  dashboardPath,
    45  				Name: dashboardConfig.Name,
    46  				Variables: dashboards.MonitoringDashboardExternalLinkVariables{
    47  					App:       dashboardConfig.Variables.App,
    48  					Namespace: dashboardConfig.Variables.Namespace,
    49  					Service:   dashboardConfig.Variables.Service,
    50  					Version:   dashboardConfig.Variables.Version,
    51  					Workload:  dashboardConfig.Variables.Workload,
    52  				},
    53  			}
    54  			links = append(links, externalLink)
    55  		}
    56  	}
    57  
    58  	grafanaInfo := models.GrafanaInfo{
    59  		ExternalLinks: links,
    60  	}
    61  
    62  	return &grafanaInfo, http.StatusOK, nil
    63  }
    64  
    65  // GetGrafanaLinks returns the links to Grafana dashboards and other info, the HTTP status code (int) and eventually an error
    66  func GetGrafanaLinks(linksSpec []dashboards.MonitoringDashboardExternalLink) ([]models.ExternalLink, int, error) {
    67  	grafanaConfig := config.Get().ExternalServices.Grafana
    68  	if !grafanaConfig.Enabled {
    69  		return nil, 0, nil
    70  	}
    71  
    72  	connectionInfo, code, err := getGrafanaConnectionInfo(&grafanaConfig)
    73  	if err != nil {
    74  		return nil, code, err
    75  	}
    76  	if connectionInfo.baseExternalURL == "" {
    77  		log.Tracef("Skip checking Grafana links as Grafana is not configured")
    78  		return nil, 0, nil
    79  	}
    80  	return getGrafanaLinks(connectionInfo, linksSpec, GrafanaDashboardSupplier)
    81  }
    82  
    83  func getGrafanaLinks(conn grafanaConnectionInfo, linksSpec []dashboards.MonitoringDashboardExternalLink, dashboardSupplier dashboardSupplier) ([]models.ExternalLink, int, error) {
    84  	// Call Grafana REST API to get dashboard urls
    85  	linksOut := []models.ExternalLink{}
    86  	for _, linkSpec := range linksSpec {
    87  		if linkSpec.Type == "grafana" {
    88  			dashboardPath, err := getDashboardPath(linkSpec.Name, conn, dashboardSupplier)
    89  			if err != nil {
    90  				return nil, http.StatusServiceUnavailable, err
    91  			}
    92  			if dashboardPath != "" {
    93  				linkOut := models.ExternalLink{
    94  					URL:       dashboardPath,
    95  					Name:      linkSpec.Name,
    96  					Variables: linkSpec.Variables,
    97  				}
    98  				linksOut = append(linksOut, linkOut)
    99  			}
   100  		}
   101  	}
   102  
   103  	return linksOut, http.StatusOK, nil
   104  }
   105  
   106  type grafanaConnectionInfo struct {
   107  	baseExternalURL   string
   108  	externalURLParams string
   109  	inClusterURL      string
   110  	auth              *config.Auth
   111  }
   112  
   113  func getGrafanaConnectionInfo(cfg *config.GrafanaConfig) (grafanaConnectionInfo, int, error) {
   114  	externalURL := status.DiscoverGrafana()
   115  	if externalURL == "" {
   116  		return grafanaConnectionInfo{}, http.StatusServiceUnavailable, errors.New("grafana URL is not set in Kiali configuration")
   117  	}
   118  
   119  	// Check if URL is valid
   120  	_, err := url.ParseRequestURI(externalURL)
   121  	if err != nil {
   122  		return grafanaConnectionInfo{}, http.StatusServiceUnavailable, errors.New("wrong format for Grafana URL: " + err.Error())
   123  	}
   124  
   125  	apiURL := externalURL
   126  
   127  	// Find the in-cluster URL to reach Grafana's REST API if properties demand so
   128  	if cfg.InClusterURL != "" {
   129  		apiURL = cfg.InClusterURL
   130  	}
   131  
   132  	urlParts := strings.Split(externalURL, "?")
   133  	externalURLParams := ""
   134  	// E.g.: http://localhost:3000?orgId=1 transformed into http://localhost:3000/d/LJ_uJAvmk/istio-service-dashboard?orgId=1
   135  	externalURL = urlParts[0]
   136  	if len(urlParts) > 1 {
   137  		externalURLParams = "?" + urlParts[1]
   138  	}
   139  
   140  	return grafanaConnectionInfo{
   141  		baseExternalURL:   externalURL,
   142  		externalURLParams: externalURLParams,
   143  		inClusterURL:      apiURL,
   144  		auth:              &cfg.Auth,
   145  	}, 0, nil
   146  }
   147  
   148  func getDashboardPath(name string, conn grafanaConnectionInfo, dashboardSupplier dashboardSupplier) (string, error) {
   149  	body, code, err := dashboardSupplier(conn.inClusterURL, url.PathEscape(name), conn.auth)
   150  	if err != nil {
   151  		return "", err
   152  	}
   153  	if code != http.StatusOK {
   154  		// Get error message
   155  		var f map[string]string
   156  		err = json.Unmarshal(body, &f)
   157  		if err != nil {
   158  			return "", fmt.Errorf("unknown error from Grafana (%d)", code)
   159  		}
   160  		message, ok := f["message"]
   161  		if !ok {
   162  			return "", fmt.Errorf("unknown error from Grafana (%d)", code)
   163  		}
   164  		return "", fmt.Errorf("error from Grafana (%d): %s", code, message)
   165  	}
   166  
   167  	// Status OK, read dashboards info
   168  	var dashboards []map[string]interface{}
   169  	err = json.Unmarshal(body, &dashboards)
   170  	if err != nil {
   171  		return "", err
   172  	}
   173  	if len(dashboards) == 0 {
   174  		log.Warningf("No Grafana dashboard found for pattern '%s'", name)
   175  		return "", nil
   176  	}
   177  	if len(dashboards) > 1 {
   178  		log.Infof("Several Grafana dashboards found for pattern '%s', picking the first one", name)
   179  	}
   180  	dashPath, ok := dashboards[0]["url"]
   181  	if !ok {
   182  		log.Warningf("URL field not found in Grafana dashboard for search pattern '%s'", name)
   183  		return "", nil
   184  	}
   185  
   186  	fullPath := dashPath.(string)
   187  	if fullPath != "" {
   188  		// Dashboard path might be an absolute URL (hence starting with cfg.URL) or a relative one, depending on grafana's "GF_SERVER_SERVE_FROM_SUB_PATH"
   189  		if !strings.HasPrefix(fullPath, conn.baseExternalURL) {
   190  			fullPath = strings.TrimSuffix(conn.baseExternalURL, "/") + "/" + strings.TrimPrefix(fullPath, "/")
   191  		}
   192  	}
   193  
   194  	return fullPath + conn.externalURLParams, nil
   195  }
   196  
   197  func findDashboard(url, searchPattern string, auth *config.Auth) ([]byte, int, error) {
   198  	urlParts := strings.Split(url, "?")
   199  	query := strings.TrimSuffix(urlParts[0], "/") + "/api/search?query=" + searchPattern
   200  	if len(urlParts) > 1 {
   201  		query = query + "&" + urlParts[1]
   202  	}
   203  	resp, code, _, err := httputil.HttpGet(query, auth, time.Second*10, nil, nil)
   204  	return resp, code, err
   205  }