cmd/frontend/internal/rule/client.go (77 lines of code) (raw):
// Copyright 2024 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package rule
import (
"context"
"encoding/json"
"errors"
"fmt"
"io"
"net/http"
"net/url"
"path"
"github.com/GoogleCloudPlatform/prometheus-engine/internal/promapi"
promapiv1 "github.com/prometheus/prometheus/web/api/v1"
)
const (
rulesPath = "/api/v1/rules"
alertsPath = "/api/v1/alerts"
)
var errRequestFailed = errors.New("request to endpoint failed")
type httpClient interface {
Do(req *http.Request) (*http.Response, error)
}
// client is a client for fetching rules and alerts from a Prometheus-compatible endpoint.
type client struct {
client httpClient
}
// newClient creates a new client.
func newClient(c httpClient) *client {
return &client{client: c}
}
// RuleGroups fetches rule-groups from a single endpoint.
func (r *client) RuleGroups(ctx context.Context, baseURL url.URL, queryString string) ([]*promapiv1.RuleGroup, error) {
resp, err := r.call(ctx, baseURL, rulesPath, queryString)
if err != nil {
return nil, fmt.Errorf("calling endpoint failed with error: %w", err)
}
var parsedResp promapi.Response[promapi.RulesResponseData]
if err := json.Unmarshal(resp, &parsedResp); err != nil {
return nil, fmt.Errorf("unmarshalling response from endpoint failed with error: %w", err)
}
return parsedResp.Data.Groups, nil
}
// Alerts fetches alerts from the endpoint.
func (r *client) Alerts(ctx context.Context, baseURL url.URL, queryString string) ([]*promapiv1.Alert, error) {
resp, err := r.call(ctx, baseURL, alertsPath, queryString)
if err != nil {
return nil, fmt.Errorf("calling endpoint failed with error: %w", err)
}
var parsedResp promapi.Response[promapi.AlertsResponseData]
if err := json.Unmarshal(resp, &parsedResp); err != nil {
return nil, fmt.Errorf("unmarshalling response from endpoint failed with error: %w", err)
}
return parsedResp.Data.Alerts, nil
}
// call calls the server with the given query string and returns the response.
func (r *client) call(ctx context.Context, baseURL url.URL, endpoint, queryString string) ([]byte, error) {
fullURL := url.URL{
Scheme: baseURL.Scheme,
Host: baseURL.Host,
Path: path.Join(baseURL.Path, endpoint),
RawQuery: queryString,
}
req, err := http.NewRequestWithContext(ctx, http.MethodGet, fullURL.String(), nil)
if err != nil {
return nil, fmt.Errorf("constructing request to endpoint %s failed with error: %w", baseURL.Path, err)
}
resp, err := r.client.Do(req)
if err != nil {
if errors.Is(err, context.Canceled) {
return nil, fmt.Errorf("request to endpoint %s was canceled: %w", baseURL.Path, err)
}
return nil, fmt.Errorf("request to endpoint %s failed with error: %w", baseURL.Path, err)
}
if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("request to endpoint %s failed with status code: %d. error: %w", baseURL.Path, resp.StatusCode, errRequestFailed)
}
defer resp.Body.Close()
rawResponse, err := io.ReadAll(resp.Body)
if err != nil {
return nil, fmt.Errorf("reading response body from endpoint %s failed with error: %w", baseURL.Path, err)
}
return rawResponse, nil
}