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 }