github.com/m3db/m3@v1.5.1-0.20231129193456-75a402aa583b/src/integration/resources/docker/dockerexternal/prometheus.go (about)

     1  // Copyright (c) 2021 Uber Technologies, Inc.
     2  //
     3  // Permission is hereby granted, free of charge, to any person obtaining a copy
     4  // of this software and associated documentation files (the "Software"), to deal
     5  // in the Software without restriction, including without limitation the rights
     6  // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
     7  // copies of the Software, and to permit persons to whom the Software is
     8  // furnished to do so, subject to the following conditions:
     9  //
    10  // The above copyright notice and this permission notice shall be included in
    11  // all copies or substantial portions of the Software.
    12  //
    13  // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
    14  // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
    15  // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
    16  // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
    17  // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
    18  // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
    19  // THE SOFTWARE.
    20  
    21  package dockerexternal
    22  
    23  import (
    24  	"context"
    25  	"encoding/json"
    26  	"errors"
    27  	"fmt"
    28  	"io/ioutil"
    29  	"net/http"
    30  	"time"
    31  
    32  	xdockertest "github.com/m3db/m3/src/x/dockertest"
    33  	"github.com/m3db/m3/src/x/instrument"
    34  
    35  	"github.com/cenkalti/backoff/v3"
    36  	"github.com/ory/dockertest/v3"
    37  	"github.com/prometheus/common/model"
    38  )
    39  
    40  // Prometheus is a docker-backed instantiation of Prometheus.
    41  type Prometheus struct {
    42  	pool      *dockertest.Pool
    43  	pathToCfg string
    44  	iOpts     instrument.Options
    45  
    46  	resource *xdockertest.Resource
    47  }
    48  
    49  // PrometheusOptions contains the options for
    50  // spinning up docker container running Prometheus
    51  type PrometheusOptions struct {
    52  	// Pool is the connection to the docker API
    53  	Pool *dockertest.Pool
    54  	// PathToCfg contains the path to the prometheus.yml configuration
    55  	// file to be used on startup.
    56  	PathToCfg string
    57  	// InstrumentOptions are the instrument.Options to use when
    58  	// creating the resource.
    59  	InstrumentOptions instrument.Options
    60  }
    61  
    62  // NewPrometheus creates a new docker-backed Prometheus
    63  // that implements the resources.ExternalResources interface.
    64  func NewPrometheus(opts PrometheusOptions) *Prometheus {
    65  	if opts.InstrumentOptions == nil {
    66  		opts.InstrumentOptions = instrument.NewOptions()
    67  	}
    68  	return &Prometheus{
    69  		pool:      opts.Pool,
    70  		pathToCfg: opts.PathToCfg,
    71  		iOpts:     opts.InstrumentOptions,
    72  	}
    73  }
    74  
    75  // Setup is a method that setups up the prometheus instance.
    76  func (p *Prometheus) Setup(ctx context.Context) error {
    77  	if p.resource != nil {
    78  		return errors.New("prometheus already setup. must close resource " +
    79  			"before attempting to setup again")
    80  	}
    81  
    82  	if err := xdockertest.SetupNetwork(p.pool, true); err != nil {
    83  		return err
    84  	}
    85  
    86  	res, err := xdockertest.NewDockerResource(p.pool, xdockertest.ResourceOptions{
    87  		ContainerName: "prometheus",
    88  		Image: xdockertest.Image{
    89  			Name: "prom/prometheus",
    90  			Tag:  "latest",
    91  		},
    92  		PortList: []int{9090},
    93  		Mounts: []string{
    94  			fmt.Sprintf("%s:/etc/prometheus/prometheus.yml", p.pathToCfg),
    95  		},
    96  		InstrumentOpts: p.iOpts,
    97  	})
    98  	if err != nil {
    99  		return err
   100  	}
   101  
   102  	p.resource = res
   103  
   104  	return p.waitForHealthy(ctx)
   105  }
   106  
   107  func (p *Prometheus) waitForHealthy(ctx context.Context) error {
   108  	retrier := backoff.WithContext(backoff.NewExponentialBackOff(), ctx)
   109  	return backoff.Retry(func() error {
   110  		req, err := http.NewRequestWithContext(
   111  			context.Background(),
   112  			http.MethodGet,
   113  			"http://0.0.0.0:9090/-/ready",
   114  			nil,
   115  		)
   116  		if err != nil {
   117  			return err
   118  		}
   119  
   120  		client := http.Client{}
   121  		res, _ := client.Do(req)
   122  		if res != nil {
   123  			_ = res.Body.Close()
   124  
   125  			if res.StatusCode == http.StatusOK {
   126  				return nil
   127  			}
   128  		}
   129  
   130  		return errors.New("prometheus not ready")
   131  	}, retrier)
   132  }
   133  
   134  // PrometheusQueryRequest contains the parameters for making a query request.
   135  type PrometheusQueryRequest struct {
   136  	// Query is the prometheus query to execute
   137  	Query string
   138  	// Time is the time to execute the query at
   139  	Time time.Time
   140  }
   141  
   142  // String converts the query request into a string suitable for use
   143  // in the url params or request body
   144  func (p *PrometheusQueryRequest) String() string {
   145  	str := fmt.Sprintf("query=%v", p.Query)
   146  
   147  	if !p.Time.IsZero() {
   148  		str += fmt.Sprintf("&time=%v", p.Time.Unix())
   149  	}
   150  
   151  	return str
   152  }
   153  
   154  // Query executes a query request against the prometheus resource.
   155  func (p *Prometheus) Query(req PrometheusQueryRequest) (model.Vector, error) {
   156  	if p.resource.Closed() {
   157  		return nil, xdockertest.ErrClosed
   158  	}
   159  
   160  	r, err := http.NewRequestWithContext(
   161  		context.Background(),
   162  		http.MethodGet,
   163  		fmt.Sprintf("http://0.0.0.0:9090/api/v1/query?%s", req.String()),
   164  		nil,
   165  	)
   166  	if err != nil {
   167  		return nil, err
   168  	}
   169  
   170  	client := http.Client{}
   171  	res, err := client.Do(r)
   172  	if err != nil {
   173  		return nil, err
   174  	}
   175  	defer res.Body.Close()
   176  
   177  	body, err := ioutil.ReadAll(res.Body)
   178  	if err != nil {
   179  		return nil, err
   180  	}
   181  
   182  	if res.StatusCode != http.StatusOK {
   183  		return nil, fmt.Errorf("non-200 status code received. "+
   184  			"status=%v responseBody=%v", res.StatusCode, string(body))
   185  	}
   186  
   187  	var parsedResp jsonInstantQueryResponse
   188  	if err := json.Unmarshal(body, &parsedResp); err != nil {
   189  		return nil, err
   190  	}
   191  
   192  	return parsedResp.Data.Result, nil
   193  }
   194  
   195  type jsonInstantQueryResponse struct {
   196  	Status string
   197  	Data   vectorResult
   198  }
   199  
   200  type vectorResult struct {
   201  	ResultType model.ValueType
   202  	Result     model.Vector
   203  }
   204  
   205  // Close cleans up the prometheus instance.
   206  func (p *Prometheus) Close(context.Context) error {
   207  	if p.resource.Closed() {
   208  		return xdockertest.ErrClosed
   209  	}
   210  
   211  	if err := p.resource.Close(); err != nil {
   212  		return err
   213  	}
   214  
   215  	p.resource = nil
   216  
   217  	return nil
   218  }