Ctrl K
ComunidadGuías…

pkg/prom/transport.go

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
package prom

import (
	"context"
	"errors"
	"fmt"
	"io"
	"net/http"
	"net/url"
	"strings"
	"time"
	"unicode"
)

var ErrResponseTooLarge = errors.New("upstream response exceeds configured limit")

// Transport is the pluggable HTTP transport used by Client to issue requests
// to a Prometheus HTTP API. Implementations decide how the request physically
// reaches Prometheus — typically either direct HTTP against a known URL
// (in-cluster, or a kubectl port-forwarded localhost) or a tunneled proxy
// transport that forwards requests through some external broker to an
// in-cluster Prometheus.
//
// Transport is responsible for returning the raw upstream body bytes. Parsing
// is the Client's concern.
type Transport interface {
	Do(ctx context.Context, method, path string, params url.Values) ([]byte, error)

	// Address returns a human-readable identifier for this transport, used
	// for status reporting and error messages — typically the base URL, or
	// a short description of the proxy path for tunneled transports.
	Address() string
}

// HTTPTransport is a direct-HTTP Transport. It targets BaseURL + BasePath +
// the request path, and uses HTTPClient to send the request.
//
// BasePath is an optional prefix applied before Prometheus API paths and is
// useful for vmselect-style deployments where the API lives under e.g.
// "/select/0/prometheus".
//
// Headers, if non-empty, are applied to every request after the default
// Accept header, so callers may override Accept by setting it here. Typical
// uses are Authorization: Bearer ... and tenant headers like X-Scope-OrgID.
type HTTPTransport struct {
	BaseURL          string
	BasePath         string
	HTTPClient       *http.Client
	Headers          map[string]string
	MaxResponseBytes int64
}

// NewHTTPTransport constructs an HTTPTransport with a default 10-second
// timeout if none is provided.
func NewHTTPTransport(baseURL, basePath string, httpClient *http.Client) *HTTPTransport {
	if httpClient == nil {
		httpClient = &http.Client{Timeout: 10 * time.Second}
	}
	return &HTTPTransport{
		BaseURL:    strings.TrimRight(baseURL, "/"),
		BasePath:   basePath,
		HTTPClient: httpClient,
	}
}

// Do issues a request and returns the response body bytes. Non-2xx status
// codes yield a *HTTPError; callers can use errors.As to extract the
// status code and upstream body (Probe distinguishes 401/403 from other
// transport errors this way, for example).
func (t *HTTPTransport) Do(ctx context.Context, method, path string, params url.Values) ([]byte, error) {
	full := t.BaseURL + t.BasePath + path
	var requestBody io.Reader
	if method == http.MethodPost {
		requestBody = strings.NewReader(params.Encode())
	} else if len(params) > 0 {
		if strings.Contains(full, "?") {
			full = full + "&" + params.Encode()
		} else {
			full = full + "?" + params.Encode()
		}
	}

	req, err := http.NewRequestWithContext(ctx, method, full, requestBody)
	if err != nil {
		return nil, fmt.Errorf("prom.HTTPTransport: build request: %w", err)
	}
	req.Header.Set("Accept", "application/json")
	for k, v := range t.Headers {
		req.Header.Set(k, v)
	}
	if method == http.MethodPost {
		req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
	}

	resp, err := SameOriginRedirectClient(t.HTTPClient).Do(req)
	if err != nil {
		return nil, fmt.Errorf("prom.HTTPTransport: %w", err)
	}
	defer func() { _ = resp.Body.Close() }()

	maxResponseBytes := t.MaxResponseBytes
	if maxResponseBytes <= 0 {
		maxResponseBytes = 10 << 20
	}
	// From here on the endpoint has answered: a failure reading the body is
	// wrapped so callers reasoning about reachability can tell it from a
	// connection that never produced a response.
	body, err := io.ReadAll(io.LimitReader(resp.Body, maxResponseBytes+1))
	if err != nil {
		return nil, fmt.Errorf("prom.HTTPTransport: read body: %w", &ResponseError{StatusCode: resp.StatusCode, Err: err})
	}
	if int64(len(body)) > maxResponseBytes {
		return nil, fmt.Errorf("prom.HTTPTransport: %w (%d bytes)", &ResponseError{StatusCode: resp.StatusCode, Err: ErrResponseTooLarge}, maxResponseBytes)
	}

	if resp.StatusCode != http.StatusOK {
		return nil, &HTTPError{StatusCode: resp.StatusCode, URL: full, Body: body}
	}
	return body, nil
}

// Address returns the effective base URL for diagnostics.
func (t *HTTPTransport) Address() string {
	return t.BaseURL + t.BasePath
}

// HTTPError is returned when an HTTP-backed metrics or cost source responds with a non-2xx status.
type HTTPError struct {
	StatusCode int
	URL        string
	Body       []byte
}

// ResponseError is a failure after the endpoint answered — the body could not
// be read, or was over the limit. It carries the status that arrived so the
// failure is never mistaken for an unreachable endpoint.
type ResponseError struct {
	StatusCode int
	Err        error
}

func (e *ResponseError) Error() string { return e.Err.Error() }
func (e *ResponseError) Unwrap() error { return e.Err }

func (e *HTTPError) Error() string {
	body := strings.ToValidUTF8(string(e.Body), "�")
	body = strings.Map(func(r rune) rune {
		if unicode.IsControl(r) || r == '\u2028' || r == '\u2029' {
			return ' '
		}
		return r
	}, body)
	const maxDiagnosticRunes = 512
	if runes := []rune(body); len(runes) > maxDiagnosticRunes {
		body = string(runes[:maxDiagnosticRunes]) + "…"
	}
	return fmt.Sprintf("upstream returned %d for %s: %s", e.StatusCode, e.URL, body)
}