Improve error handling for validator

- Surface error codes for datasource possible errors (not found, unreachabel, auth, timeout)
This commit is contained in:
alexandra vargas
2026-01-06 13:27:34 +01:00
parent f81deced02
commit 01f959be97
5 changed files with 600 additions and 23 deletions
+93 -8
View File
@@ -19,6 +19,7 @@ import (
"github.com/grafana/grafana/pkg/infra/httpclient"
"github.com/grafana/grafana/pkg/services/datasources"
"github.com/grafana/grafana/pkg/services/pluginsintegration/plugincontext"
"strings"
)
type DashValidatorConfig struct {
@@ -143,35 +144,80 @@ func handleCheckRoute(
logger.Info("Processing request", "dashboardTitle", req.DashboardJSON["title"], "numMappings", len(req.DatasourceMappings))
// Get namespace from request (needed for datasource lookup)
// Namespace format is typically "org-{orgID}"
namespace := r.ResourceIdentifier.Namespace
// Extract orgID from namespace for logging context
orgID := extractOrgIDFromNamespace(namespace)
logger = logger.With("orgID", orgID, "namespace", namespace)
for _, dsMapping := range req.DatasourceMappings {
dsLogger := logger.With("datasourceUID", dsMapping.UID, "datasourceType", dsMapping.Type)
// Convert optional name pointer to string
name := ""
if dsMapping.Name != nil {
name = *dsMapping.Name
dsLogger = dsLogger.With("datasourceName", name)
}
// Fetch datasource from Grafana using app-platform method
// Parameters: namespace, name (UID), group (datasource type)
ds, err := datasourceSvc.GetDataSourceInNamespace(ctx, namespace, dsMapping.UID, dsMapping.Type)
if err != nil {
logger.Error("Failed to get datasource", "namespace", namespace, "uid", dsMapping.UID, "type", dsMapping.Type, "error", err)
w.WriteHeader(http.StatusBadRequest)
dsLogger.Error("Failed to get datasource from namespace", "error", err)
// Check if it's a not found error vs other errors
errMsg := err.Error()
statusCode := http.StatusInternalServerError
userMsg := fmt.Sprintf("failed to retrieve datasource: %s", dsMapping.UID)
if strings.Contains(errMsg, "not found") || strings.Contains(errMsg, "does not exist") {
statusCode = http.StatusNotFound
userMsg = fmt.Sprintf("datasource not found: %s (type: %s)", dsMapping.UID, dsMapping.Type)
dsLogger.Warn("Datasource not found in namespace")
}
w.WriteHeader(statusCode)
return json.NewEncoder(w).Encode(map[string]string{
"error": fmt.Sprintf("datasource not found: %s", dsMapping.UID),
"error": userMsg,
"code": "datasource_error",
})
}
logger.Info("Retrieved datasource", "uid", ds.UID, "url", ds.URL, "type", ds.Type)
dsLogger.Info("Retrieved datasource", "url", ds.URL, "actualType", ds.Type)
// Validate that the datasource type matches the expected type
if ds.Type != dsMapping.Type {
dsLogger.Error("Datasource type mismatch",
"expectedType", dsMapping.Type,
"actualType", ds.Type)
w.WriteHeader(http.StatusBadRequest)
return json.NewEncoder(w).Encode(map[string]string{
"error": fmt.Sprintf("datasource %s has type %s, expected %s", dsMapping.UID, ds.Type, dsMapping.Type),
"code": "datasource_wrong_type",
})
}
// Validate that this is a supported datasource type
// For MVP, we only support Prometheus
if !isSupportedDatasourceType(ds.Type) {
dsLogger.Error("Unsupported datasource type", "type", ds.Type)
w.WriteHeader(http.StatusBadRequest)
return json.NewEncoder(w).Encode(map[string]string{
"error": fmt.Sprintf("datasource type '%s' is not supported (currently only 'prometheus' is supported)", ds.Type),
"code": "datasource_unsupported_type",
})
}
// Get authenticated HTTP transport for this datasource
transport, err := datasourceSvc.GetHTTPTransport(ctx, ds, httpClientProvider)
if err != nil {
logger.Error("Failed to get HTTP transport", "uid", ds.UID, "error", err)
dsLogger.Error("Failed to get HTTP transport for datasource", "error", err)
w.WriteHeader(http.StatusInternalServerError)
return json.NewEncoder(w).Encode(map[string]string{
"error": fmt.Sprintf("failed to configure datasource transport: %s", dsMapping.UID),
"error": fmt.Sprintf("failed to configure authentication for datasource: %s", dsMapping.UID),
"code": "datasource_config_error",
})
}
@@ -187,15 +233,35 @@ func handleCheckRoute(
URL: ds.URL,
HTTPClient: httpClient, // Pass authenticated client
})
dsLogger.Debug("Datasource configured successfully for validation")
}
// Step 3: Validate dashboard compatibility
result, err := validator.ValidateDashboardCompatibility(ctx, validatorReq)
if err != nil {
logger.Error("Validation failed", "error", err)
w.WriteHeader(http.StatusInternalServerError)
// Check if it's a structured ValidationError with a specific status code
statusCode := http.StatusInternalServerError
errorCode := "validation_error"
errorMsg := fmt.Sprintf("validation failed: %v", err)
if validationErr := validator.GetValidationError(err); validationErr != nil {
statusCode = validationErr.StatusCode
errorCode = string(validationErr.Code)
errorMsg = validationErr.Message
// Log additional context from the error
for key, value := range validationErr.Details {
logger.Error("Validation error detail", key, value)
}
}
w.WriteHeader(statusCode)
return json.NewEncoder(w).Encode(map[string]string{
"error": fmt.Sprintf("validation failed: %v", err),
"error": errorMsg,
"code": errorCode,
})
}
@@ -253,6 +319,25 @@ func convertToCheckResponse(result *validator.DashboardCompatibilityResult) chec
return response
}
// extractOrgIDFromNamespace extracts the org ID from a namespace string
// Namespace format is typically "org-{orgID}"
func extractOrgIDFromNamespace(namespace string) string {
parts := strings.Split(namespace, "-")
if len(parts) >= 2 && parts[0] == "org" {
return parts[1]
}
return "unknown"
}
// isSupportedDatasourceType checks if a datasource type is supported
// For MVP, we only support Prometheus
func isSupportedDatasourceType(dsType string) bool {
supportedTypes := map[string]bool{
"prometheus": true,
}
return supportedTypes[strings.ToLower(dsType)]
}
func GetKinds() map[schema.GroupVersion][]resource.Kind {
gv := schema.GroupVersion{
Group: "dashvalidator.grafana.com",
@@ -106,8 +106,9 @@ func ValidateDashboardCompatibility(ctx context.Context, req DashboardCompatibil
// Validate queries
validationResult, err := v.ValidateQueries(ctx, dsQueries, ds)
if err != nil {
// Validation failed for this datasource, skip but could log
continue
// Validation failed for this datasource - return error to caller
// This could be a connection error, auth error, or other critical failure
return nil, fmt.Errorf("validation failed for datasource %s: %w", dsMapping.UID, err)
}
// Convert to DatasourceValidationResult
+173
View File
@@ -0,0 +1,173 @@
package validator
import (
"errors"
"fmt"
"net/http"
)
// ErrorCode represents the type of error that occurred
type ErrorCode string
const (
// Datasource-related errors
ErrCodeDatasourceNotFound ErrorCode = "datasource_not_found"
ErrCodeDatasourceWrongType ErrorCode = "datasource_wrong_type"
ErrCodeDatasourceUnreachable ErrorCode = "datasource_unreachable"
ErrCodeDatasourceAuth ErrorCode = "datasource_auth_failed"
ErrCodeDatasourceConfig ErrorCode = "datasource_config_error"
// API-related errors
ErrCodeAPIUnavailable ErrorCode = "api_unavailable"
ErrCodeAPIInvalidResponse ErrorCode = "api_invalid_response"
ErrCodeAPIRateLimit ErrorCode = "api_rate_limit"
ErrCodeAPITimeout ErrorCode = "api_timeout"
// Validation errors
ErrCodeInvalidDashboard ErrorCode = "invalid_dashboard"
ErrCodeUnsupportedDashVersion ErrorCode = "unsupported_dashboard_version"
ErrCodeInvalidQuery ErrorCode = "invalid_query"
// Internal errors
ErrCodeInternal ErrorCode = "internal_error"
)
// ValidationError represents a structured error with context
type ValidationError struct {
Code ErrorCode
Message string
Details map[string]interface{}
StatusCode int
Cause error
}
// Error implements the error interface
func (e *ValidationError) Error() string {
if e.Cause != nil {
return fmt.Sprintf("%s: %s (caused by: %v)", e.Code, e.Message, e.Cause)
}
return fmt.Sprintf("%s: %s", e.Code, e.Message)
}
// Unwrap implements error unwrapping
func (e *ValidationError) Unwrap() error {
return e.Cause
}
// NewValidationError creates a new ValidationError
func NewValidationError(code ErrorCode, message string, statusCode int) *ValidationError {
return &ValidationError{
Code: code,
Message: message,
StatusCode: statusCode,
Details: make(map[string]interface{}),
}
}
// WithCause adds the underlying error cause
func (e *ValidationError) WithCause(err error) *ValidationError {
e.Cause = err
return e
}
// WithDetail adds contextual information
func (e *ValidationError) WithDetail(key string, value interface{}) *ValidationError {
e.Details[key] = value
return e
}
// Common error constructors
// NewDatasourceNotFoundError creates an error for datasource not found
func NewDatasourceNotFoundError(uid string, namespace string) *ValidationError {
return NewValidationError(
ErrCodeDatasourceNotFound,
fmt.Sprintf("datasource not found: %s", uid),
http.StatusNotFound,
).WithDetail("datasourceUID", uid).WithDetail("namespace", namespace)
}
// NewDatasourceWrongTypeError creates an error for wrong datasource type
func NewDatasourceWrongTypeError(uid string, expectedType string, actualType string) *ValidationError {
return NewValidationError(
ErrCodeDatasourceWrongType,
fmt.Sprintf("datasource %s has wrong type: expected %s, got %s", uid, expectedType, actualType),
http.StatusBadRequest,
).WithDetail("datasourceUID", uid).
WithDetail("expectedType", expectedType).
WithDetail("actualType", actualType)
}
// NewDatasourceUnreachableError creates an error for unreachable datasource
func NewDatasourceUnreachableError(uid string, url string, cause error) *ValidationError {
return NewValidationError(
ErrCodeDatasourceUnreachable,
fmt.Sprintf("datasource %s at %s is unreachable", uid, url),
http.StatusServiceUnavailable,
).WithDetail("datasourceUID", uid).
WithDetail("url", url).
WithCause(cause)
}
// NewAPIUnavailableError creates an error for unavailable API
func NewAPIUnavailableError(statusCode int, responseBody string, cause error) *ValidationError {
return NewValidationError(
ErrCodeAPIUnavailable,
fmt.Sprintf("Prometheus API returned status %d", statusCode),
http.StatusBadGateway,
).WithDetail("upstreamStatus", statusCode).
WithDetail("responseBody", responseBody).
WithCause(cause)
}
// NewAPIInvalidResponseError creates an error for invalid API response
func NewAPIInvalidResponseError(message string, cause error) *ValidationError {
return NewValidationError(
ErrCodeAPIInvalidResponse,
fmt.Sprintf("Prometheus API returned invalid response: %s", message),
http.StatusBadGateway,
).WithCause(cause)
}
// NewAPITimeoutError creates an error for API timeout
func NewAPITimeoutError(url string, cause error) *ValidationError {
return NewValidationError(
ErrCodeAPITimeout,
fmt.Sprintf("request to %s timed out", url),
http.StatusGatewayTimeout,
).WithDetail("url", url).
WithCause(cause)
}
// NewDatasourceAuthError creates an error for authentication failures
func NewDatasourceAuthError(uid string, statusCode int) *ValidationError {
return NewValidationError(
ErrCodeDatasourceAuth,
fmt.Sprintf("authentication failed for datasource %s (status %d)", uid, statusCode),
http.StatusUnauthorized,
).WithDetail("datasourceUID", uid).
WithDetail("upstreamStatus", statusCode)
}
// IsValidationError checks if an error is a ValidationError
func IsValidationError(err error) bool {
var validationErr *ValidationError
return errors.As(err, &validationErr)
}
// GetValidationError extracts a ValidationError from an error chain
func GetValidationError(err error) *ValidationError {
var validationErr *ValidationError
if errors.As(err, &validationErr) {
return validationErr
}
return nil
}
// GetHTTPStatusCode returns the appropriate HTTP status code for an error
func GetHTTPStatusCode(err error) int {
if validationErr := GetValidationError(err); validationErr != nil {
return validationErr.StatusCode
}
return http.StatusInternalServerError
}
@@ -0,0 +1,245 @@
package validator
import (
"errors"
"net/http"
"testing"
)
func TestNewDatasourceNotFoundError(t *testing.T) {
err := NewDatasourceNotFoundError("test-uid", "org-1")
if err.Code != ErrCodeDatasourceNotFound {
t.Errorf("expected error code %s, got %s", ErrCodeDatasourceNotFound, err.Code)
}
if err.StatusCode != http.StatusNotFound {
t.Errorf("expected status code %d, got %d", http.StatusNotFound, err.StatusCode)
}
if err.Details["datasourceUID"] != "test-uid" {
t.Errorf("expected datasourceUID detail to be 'test-uid', got %v", err.Details["datasourceUID"])
}
if err.Details["namespace"] != "org-1" {
t.Errorf("expected namespace detail to be 'org-1', got %v", err.Details["namespace"])
}
}
func TestNewDatasourceWrongTypeError(t *testing.T) {
err := NewDatasourceWrongTypeError("test-uid", "prometheus", "influxdb")
if err.Code != ErrCodeDatasourceWrongType {
t.Errorf("expected error code %s, got %s", ErrCodeDatasourceWrongType, err.Code)
}
if err.StatusCode != http.StatusBadRequest {
t.Errorf("expected status code %d, got %d", http.StatusBadRequest, err.StatusCode)
}
if err.Details["expectedType"] != "prometheus" {
t.Errorf("expected expectedType detail to be 'prometheus', got %v", err.Details["expectedType"])
}
if err.Details["actualType"] != "influxdb" {
t.Errorf("expected actualType detail to be 'influxdb', got %v", err.Details["actualType"])
}
}
func TestNewDatasourceUnreachableError(t *testing.T) {
cause := errors.New("connection refused")
err := NewDatasourceUnreachableError("test-uid", "http://localhost:9090", cause)
if err.Code != ErrCodeDatasourceUnreachable {
t.Errorf("expected error code %s, got %s", ErrCodeDatasourceUnreachable, err.Code)
}
if err.StatusCode != http.StatusServiceUnavailable {
t.Errorf("expected status code %d, got %d", http.StatusServiceUnavailable, err.StatusCode)
}
if err.Cause != cause {
t.Errorf("expected cause to be set")
}
if err.Details["url"] != "http://localhost:9090" {
t.Errorf("expected url detail to be 'http://localhost:9090', got %v", err.Details["url"])
}
}
func TestNewAPIUnavailableError(t *testing.T) {
err := NewAPIUnavailableError(503, "service unavailable", nil)
if err.Code != ErrCodeAPIUnavailable {
t.Errorf("expected error code %s, got %s", ErrCodeAPIUnavailable, err.Code)
}
if err.StatusCode != http.StatusBadGateway {
t.Errorf("expected status code %d, got %d", http.StatusBadGateway, err.StatusCode)
}
if err.Details["upstreamStatus"] != 503 {
t.Errorf("expected upstreamStatus detail to be 503, got %v", err.Details["upstreamStatus"])
}
}
func TestNewAPIInvalidResponseError(t *testing.T) {
cause := errors.New("invalid JSON")
err := NewAPIInvalidResponseError("missing data field", cause)
if err.Code != ErrCodeAPIInvalidResponse {
t.Errorf("expected error code %s, got %s", ErrCodeAPIInvalidResponse, err.Code)
}
if err.StatusCode != http.StatusBadGateway {
t.Errorf("expected status code %d, got %d", http.StatusBadGateway, err.StatusCode)
}
if err.Cause != cause {
t.Errorf("expected cause to be set")
}
}
func TestNewAPITimeoutError(t *testing.T) {
cause := errors.New("context deadline exceeded")
err := NewAPITimeoutError("http://localhost:9090/api/v1/query", cause)
if err.Code != ErrCodeAPITimeout {
t.Errorf("expected error code %s, got %s", ErrCodeAPITimeout, err.Code)
}
if err.StatusCode != http.StatusGatewayTimeout {
t.Errorf("expected status code %d, got %d", http.StatusGatewayTimeout, err.StatusCode)
}
if err.Cause != cause {
t.Errorf("expected cause to be set")
}
}
func TestNewDatasourceAuthError(t *testing.T) {
err := NewDatasourceAuthError("test-uid", 401)
if err.Code != ErrCodeDatasourceAuth {
t.Errorf("expected error code %s, got %s", ErrCodeDatasourceAuth, err.Code)
}
if err.StatusCode != http.StatusUnauthorized {
t.Errorf("expected status code %d, got %d", http.StatusUnauthorized, err.StatusCode)
}
if err.Details["upstreamStatus"] != 401 {
t.Errorf("expected upstreamStatus detail to be 401, got %v", err.Details["upstreamStatus"])
}
}
func TestValidationErrorChaining(t *testing.T) {
cause := errors.New("network error")
err := NewValidationError(ErrCodeInternal, "test error", http.StatusInternalServerError).
WithCause(cause).
WithDetail("key1", "value1").
WithDetail("key2", 123)
if err.Cause != cause {
t.Errorf("expected cause to be set")
}
if err.Details["key1"] != "value1" {
t.Errorf("expected detail key1 to be 'value1', got %v", err.Details["key1"])
}
if err.Details["key2"] != 123 {
t.Errorf("expected detail key2 to be 123, got %v", err.Details["key2"])
}
}
func TestIsValidationError(t *testing.T) {
validationErr := NewDatasourceNotFoundError("test-uid", "org-1")
regularErr := errors.New("regular error")
if !IsValidationError(validationErr) {
t.Errorf("expected IsValidationError to return true for ValidationError")
}
if IsValidationError(regularErr) {
t.Errorf("expected IsValidationError to return false for regular error")
}
}
func TestGetValidationError(t *testing.T) {
validationErr := NewDatasourceNotFoundError("test-uid", "org-1")
regularErr := errors.New("regular error")
retrieved := GetValidationError(validationErr)
if retrieved == nil {
t.Errorf("expected GetValidationError to return the ValidationError")
}
if retrieved.Code != ErrCodeDatasourceNotFound {
t.Errorf("expected retrieved error to have correct code")
}
retrieved = GetValidationError(regularErr)
if retrieved != nil {
t.Errorf("expected GetValidationError to return nil for regular error")
}
}
func TestGetHTTPStatusCode(t *testing.T) {
validationErr := NewDatasourceNotFoundError("test-uid", "org-1")
regularErr := errors.New("regular error")
statusCode := GetHTTPStatusCode(validationErr)
if statusCode != http.StatusNotFound {
t.Errorf("expected status code %d, got %d", http.StatusNotFound, statusCode)
}
statusCode = GetHTTPStatusCode(regularErr)
if statusCode != http.StatusInternalServerError {
t.Errorf("expected default status code %d for regular error, got %d", http.StatusInternalServerError, statusCode)
}
}
func TestErrorUnwrap(t *testing.T) {
cause := errors.New("underlying error")
err := NewDatasourceUnreachableError("test-uid", "http://localhost:9090", cause)
unwrapped := errors.Unwrap(err)
if unwrapped != cause {
t.Errorf("expected Unwrap to return the cause")
}
}
func TestErrorErrorMethod(t *testing.T) {
// Test without cause
err1 := NewDatasourceNotFoundError("test-uid", "org-1")
errMsg1 := err1.Error()
if errMsg1 == "" {
t.Errorf("expected non-empty error message")
}
// Test with cause
cause := errors.New("underlying error")
err2 := NewDatasourceUnreachableError("test-uid", "http://localhost:9090", cause)
errMsg2 := err2.Error()
if errMsg2 == "" {
t.Errorf("expected non-empty error message")
}
// Error message should include the cause
if !contains(errMsg2, "underlying error") {
t.Errorf("expected error message to include cause, got: %s", errMsg2)
}
}
// Helper function to check if a string contains a substring
func contains(s, substr string) bool {
return len(s) >= len(substr) && (s == substr || len(s) > len(substr) && containsHelper(s, substr))
}
func containsHelper(s, substr string) bool {
for i := 0; i <= len(s)-len(substr); i++ {
if s[i:i+len(substr)] == substr {
return true
}
}
return false
}
@@ -3,10 +3,14 @@ package prometheus
import (
"context"
"encoding/json"
"errors"
"fmt"
"io"
"net/http"
"net/url"
"strings"
"github.com/grafana/grafana/apps/dashvalidator/pkg/validator"
)
// Fetcher fetches available metrics from a Prometheus datasource
@@ -31,13 +35,22 @@ func (f *Fetcher) FetchMetrics(ctx context.Context, datasourceURL string, client
// Build the API URL
baseURL, err := url.Parse(datasourceURL)
if err != nil {
return nil, fmt.Errorf("invalid datasource URL: %w", err)
return nil, validator.NewValidationError(
validator.ErrCodeDatasourceConfig,
"invalid datasource URL",
http.StatusBadRequest,
).WithCause(err).WithDetail("url", datasourceURL)
}
// Prometheus metrics endpoint
apiPath, err := url.Parse("/api/v1/label/__name__/values")
if err != nil {
return nil, fmt.Errorf("failed to parse API path: %w", err)
// This should never happen with a hardcoded path, but handle it anyway
return nil, validator.NewValidationError(
validator.ErrCodeInternal,
"failed to parse API path",
http.StatusInternalServerError,
).WithCause(err)
}
fullURL := baseURL.ResolveReference(apiPath)
@@ -45,31 +58,91 @@ func (f *Fetcher) FetchMetrics(ctx context.Context, datasourceURL string, client
// Create the request
req, err := http.NewRequestWithContext(ctx, http.MethodGet, fullURL.String(), nil)
if err != nil {
return nil, fmt.Errorf("failed to create request: %w", err)
return nil, validator.NewValidationError(
validator.ErrCodeInternal,
"failed to create HTTP request",
http.StatusInternalServerError,
).WithCause(err)
}
// Execute the request using the provided authenticated client
resp, err := client.Do(req)
if err != nil {
return nil, fmt.Errorf("failed to fetch metrics from Prometheus: %w", err)
// Check if it's a timeout error
if errors.Is(err, context.DeadlineExceeded) || strings.Contains(err.Error(), "timeout") {
return nil, validator.NewAPITimeoutError(fullURL.String(), err)
}
// Network or connection error - datasource is unreachable
return nil, validator.NewDatasourceUnreachableError("", datasourceURL, err)
}
defer resp.Body.Close()
// Check status code
if resp.StatusCode != http.StatusOK {
body, _ := io.ReadAll(resp.Body)
return nil, fmt.Errorf("Prometheus API returned status %d: %s", resp.StatusCode, string(body))
// Read response body for error reporting
body, readErr := io.ReadAll(resp.Body)
if readErr != nil {
body = []byte("<unable to read response body>")
}
// Parse the response
// Check HTTP status code
switch resp.StatusCode {
case http.StatusOK:
// Success - continue to parse response
case http.StatusUnauthorized, http.StatusForbidden:
// Authentication or authorization failure
return nil, validator.NewDatasourceAuthError("", resp.StatusCode).
WithDetail("url", fullURL.String()).
WithDetail("responseBody", string(body))
case http.StatusNotFound:
// Endpoint not found - might not be a valid Prometheus instance
return nil, validator.NewAPIUnavailableError(
resp.StatusCode,
string(body),
fmt.Errorf("endpoint not found - this may not be a valid Prometheus datasource"),
).WithDetail("url", fullURL.String())
case http.StatusTooManyRequests:
// Rate limiting
return nil, validator.NewValidationError(
validator.ErrCodeAPIRateLimit,
"Prometheus API rate limit exceeded",
http.StatusTooManyRequests,
).WithDetail("url", fullURL.String()).WithDetail("responseBody", string(body))
case http.StatusServiceUnavailable, http.StatusBadGateway, http.StatusGatewayTimeout:
// Upstream service is down or unavailable
return nil, validator.NewAPIUnavailableError(resp.StatusCode, string(body), nil).
WithDetail("url", fullURL.String())
default:
// Other error status codes
return nil, validator.NewAPIUnavailableError(resp.StatusCode, string(body), nil).
WithDetail("url", fullURL.String())
}
// Parse the response JSON
var promResp prometheusResponse
if err := json.NewDecoder(resp.Body).Decode(&promResp); err != nil {
return nil, fmt.Errorf("failed to decode Prometheus response: %w", err)
if err := json.Unmarshal(body, &promResp); err != nil {
return nil, validator.NewAPIInvalidResponseError(
"response is not valid JSON",
err,
).WithDetail("url", fullURL.String()).WithDetail("responseBody", string(body))
}
// Check Prometheus API status
// Check Prometheus API status field
if promResp.Status != "success" {
return nil, fmt.Errorf("Prometheus API returned error: %s", promResp.Error)
errorMsg := promResp.Error
if errorMsg == "" {
errorMsg = "unknown error"
}
return nil, validator.NewAPIInvalidResponseError(
fmt.Sprintf("Prometheus API returned error status: %s", errorMsg),
nil,
).WithDetail("url", fullURL.String()).WithDetail("prometheusError", errorMsg)
}
// Validate that we got data
if promResp.Data == nil {
return nil, validator.NewAPIInvalidResponseError(
"response missing 'data' field",
nil,
).WithDetail("url", fullURL.String()).WithDetail("responseBody", string(body))
}
return promResp.Data, nil